diff --git a/osu.Android.props b/osu.Android.props index d684b73d8d..0b41d5cda4 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -53,7 +53,7 @@ - - + + diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj index 9559d13328..0b2862e5bb 100644 --- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj +++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index b5497ea89f..90a6e609f0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -4,7 +4,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; -using System; +using System.Linq; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; @@ -14,12 +14,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { public class CatchBeatmapConverter : BeatmapConverter { - public CatchBeatmapConverter(IBeatmap beatmap) - : base(beatmap) + public CatchBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) { } - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index b8a844cb86..e5c3647f99 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -24,13 +24,14 @@ using System; namespace osu.Game.Rulesets.Catch { - public class CatchRuleset : Ruleset + public class CatchRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableCatchRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new CatchScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap, this); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new CatchBeatmapConverter(beatmap); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new CatchBeatmapProcessor(beatmap); public const string SHORT_NAME = "fruits"; @@ -106,6 +107,12 @@ namespace osu.Game.Rulesets.Catch new CatchModFlashlight(), }; + case ModType.Conversion: + return new Mod[] + { + new CatchModDifficultyAdjust(), + }; + case ModType.Automation: return new Mod[] { @@ -128,13 +135,15 @@ namespace osu.Game.Rulesets.Catch public override string ShortName => SHORT_NAME; + public override string PlayingVerb => "Catching fruit"; + public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch }; public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score); - public override int? LegacyID => 2; + public int LegacyID => 2; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs new file mode 100644 index 0000000000..8377b3786a --- /dev/null +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -0,0 +1,49 @@ +// 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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Catch.Mods +{ + public class CatchModDifficultyAdjust : ModDifficultyAdjust + { + [SettingSource("Fruit Size", "Override a beatmap's set CS.")] + public BindableNumber CircleSize { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + public BindableNumber ApproachRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + protected override void TransferSettings(BeatmapDifficulty difficulty) + { + base.TransferSettings(difficulty); + + TransferSetting(CircleSize, difficulty.CircleSize); + TransferSetting(ApproachRate, difficulty.ApproachRate); + } + + protected override void ApplySettings(BeatmapDifficulty difficulty) + { + base.ApplySettings(difficulty); + + difficulty.CircleSize = CircleSize.Value; + difficulty.ApproachRate = ApproachRate.Value; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index f67ca1213e..4c7bc4ab73 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -1,40 +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.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - private float hpDrainRate; - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - { - switch (result.Type) - { - case HitResult.Miss: - return hpDrainRate; - - default: - return 10.2 - hpDrainRate; // Award less HP as drain rate is increased - } - } - public override HitWindows CreateHitWindows() => new CatchHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs new file mode 100644 index 0000000000..80b1b3df8e --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/SkinnableTestScene.cs @@ -0,0 +1,46 @@ +// 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.Audio; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public abstract class SkinnableTestScene : OsuGridTestScene + { + private Skin defaultSkin; + + protected SkinnableTestScene() + : base(1, 2) + { + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, SkinManager skinManager) + { + defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); + } + + public void SetContents(Func creationFunction) + { + Cell(0).Child = createProvider(null, creationFunction); + Cell(1).Child = createProvider(defaultSkin, creationFunction); + } + + private Drawable createProvider(Skin skin, Func creationFunction) + { + var mainProvider = new SkinProvidingContainer(skin); + + return mainProvider + .WithChild(new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider)) + { + Child = creationFunction() + }); + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs new file mode 100644 index 0000000000..eea1a36a19 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneDrawableJudgement.cs @@ -0,0 +1,37 @@ +// 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; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneDrawableJudgement : SkinnableTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(DrawableJudgement), + typeof(DrawableManiaJudgement) + }; + + public TestSceneDrawableJudgement() + { + foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) + { + AddStep("Show " + result.GetDescription(), () => SetContents(() => + new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + })); + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs new file mode 100644 index 0000000000..7b0cf40d45 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -0,0 +1,314 @@ +// 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 NUnit.Framework; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneHoldNoteInput : RateAdjustedBeatmapTestScene + { + private const double time_before_head = 250; + private const double time_head = 1500; + private const double time_during_hold_1 = 2500; + private const double time_tail = 4000; + private const double time_after_tail = 5250; + + private List judgementResults; + private bool allJudgedFired; + + /// + /// -----[ ]----- + /// o o + /// + [Test] + public void TestNoInput() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + assertNoteJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail, ManiaAction.Key1), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressTooEarlyAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressTooEarlyThenPressAtStartAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_before_head, ManiaAction.Key1), + new ManiaReplayFrame(time_before_head + 10), + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Perfect); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAtStartAndBreak() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// xo x o o + /// + [Test] + public void TestPressAtStartThenBreakThenRepressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_head, ManiaAction.Key1), + new ManiaReplayFrame(time_head + 10), + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// x o + /// + [Test] + public void TestPressDuringNoteAndReleaseAfterTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_after_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Miss); + } + + /// + /// -----[ ]----- + /// x o o + /// + [Test] + public void TestPressDuringNoteAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_during_hold_1, ManiaAction.Key1), + new ManiaReplayFrame(time_tail), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Perfect); + assertTailJudgement(HitResult.Meh); + } + + /// + /// -----[ ]----- + /// xo o + /// + [Test] + public void TestPressAndReleaseAtTail() + { + performTest(new List + { + new ManiaReplayFrame(time_tail, ManiaAction.Key1), + new ManiaReplayFrame(time_tail + 10), + }); + + assertHeadJudgement(HitResult.Miss); + assertTickJudgement(HitResult.Miss); + assertTailJudgement(HitResult.Meh); + } + + private void assertHeadJudgement(HitResult result) + => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); + + private void assertTailJudgement(HitResult result) + => AddAssert($"tail judged as {result}", () => judgementResults[^2].Type == result); + + private void assertNoteJudgement(HitResult result) + => AddAssert($"hold note judged as {result}", () => judgementResults[^1].Type == result); + + private void assertTickJudgement(HitResult result) + => AddAssert($"tick judged as {result}", () => judgementResults[6].Type == result); // arbitrary tick + + private ScoreAccessibleReplayPlayer currentPlayer; + + private void performTest(List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = time_head, + Duration = time_tail - time_head, + Column = 0, + } + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + p.ScoreProcessor.AllJudged += () => + { + if (currentPlayer == p) allJudgedFired = true; + }; + }; + + LoadScreen(currentPlayer = p); + allJudgedFired = false; + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for all judged", () => allJudgedFired); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.cs new file mode 100644 index 0000000000..cd25d162d0 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayer.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 osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestScenePlayer : PlayerTestScene + { + public TestScenePlayer() + : base(new ManiaRuleset()) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj index dea6e6c0fb..9d362e5819 100644 --- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj +++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 6e4491de94..1a77a4944b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private const int max_notes_for_density = 7; - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - public int TargetColumns; public bool Dual; public readonly bool IsForCurrentRuleset; @@ -37,10 +35,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private ManiaBeatmap beatmap; - public ManiaBeatmapConverter(IBeatmap beatmap) - : base(beatmap) + public ManiaBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) { - IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo); + IsForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); var roundedCircleSize = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize); var roundedOverallDifficulty = Math.Round(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); @@ -69,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps } } + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition || h is ManiaHitObject); + protected override Beatmap ConvertBeatmap(IBeatmap original) { BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs index acce41db6f..4e73883de0 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteNoteSelectionBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints // Todo: This shouldn't exist, mania should not reference the drawable hitobject directly. if (DrawableObject.IsLoaded) { - DrawableNote note = position == HoldNotePosition.Start ? DrawableObject.Head : DrawableObject.Tail; + DrawableNote note = position == HoldNotePosition.Start ? (DrawableNote)DrawableObject.Head : DrawableObject.Tail; Anchor = note.Anchor; Origin = note.Origin; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bf630cf892..02c2158383 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -26,18 +26,20 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mania { - public class ManiaRuleset : Ruleset + public class ManiaRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableManiaRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ManiaScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score); @@ -45,6 +47,8 @@ namespace osu.Game.Rulesets.Mania public override HitObjectComposer CreateHitObjectComposer() => new ManiaHitObjectComposer(this); + public override ISkin CreateLegacySkinProvider(ISkinSource source) => new ManiaLegacySkinTransformer(source); + public override IEnumerable ConvertLegacyMods(LegacyMods mods) { if (mods.HasFlag(LegacyMods.Nightcore)) @@ -151,6 +155,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModRandom(), new ManiaModDualStages(), new ManiaModMirror(), + new ManiaModDifficultyAdjust(), }; case ModType.Automation: @@ -174,11 +179,13 @@ namespace osu.Game.Rulesets.Mania public override string ShortName => SHORT_NAME; + public override string PlayingVerb => "Smashing keys"; + public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetMania }; public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap); - public override int? LegacyID => 3; + public int LegacyID => 3; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs new file mode 100644 index 0000000000..0817f8f9fc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDifficultyAdjust.cs @@ -0,0 +1,11 @@ +// 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.Mods; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModDifficultyAdjust : ModDifficultyAdjust + { + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 87b9633c80..155adb958b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -21,11 +20,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { public override bool DisplayResult => false; - public DrawableNote Head => headContainer.Child; - public DrawableNote Tail => tailContainer.Child; + public DrawableHoldNoteHead Head => headContainer.Child; + public DrawableHoldNoteTail Tail => tailContainer.Child; - private readonly Container headContainer; - private readonly Container tailContainer; + private readonly Container headContainer; + private readonly Container tailContainer; private readonly Container tickContainer; private readonly BodyPiece bodyPiece; @@ -33,12 +32,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// - private double? holdStartTime; + public double? HoldStartTime { get; private set; } /// /// Whether the hold note has been released too early and shouldn't give full score for the release. /// - private bool hasBroken; + public bool HasBroken { get; private set; } public DrawableHoldNote(HoldNote hitObject) : base(hitObject) @@ -49,8 +48,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { bodyPiece = new BodyPiece { RelativeSizeAxes = Axes.X }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + headContainer = new Container { RelativeSizeAxes = Axes.Both }, + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); AccentColour.BindValueChanged(colour => @@ -65,11 +64,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables switch (hitObject) { - case DrawableHeadNote head: + case DrawableHoldNoteHead head: headContainer.Child = head; break; - case DrawableTailNote tail: + case DrawableHoldNoteTail tail: tailContainer.Child = tail; break; @@ -92,7 +91,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables switch (hitObject) { case TailNote _: - return new DrawableTailNote(this) + return new DrawableHoldNoteTail(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -100,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }; case Note _: - return new DrawableHeadNote(this) + return new DrawableHoldNoteHead(this) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -110,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables case HoldNoteTick tick: return new DrawableHoldNoteTick(tick) { - HoldStartTime = () => holdStartTime, + HoldStartTime = () => HoldStartTime, AccentColour = { BindTarget = AccentColour } }; } @@ -125,12 +124,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - if (Tail.AllJudged) - ApplyResult(r => r.Type = HitResult.Perfect); - } - protected override void Update() { base.Update(); @@ -146,146 +139,64 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.UpdateStateTransforms(state); } - protected void BeginHold() + protected override void CheckForResult(bool userTriggered, double timeOffset) { - holdStartTime = Time.Current; - bodyPiece.Hitting = true; - } + if (Tail.AllJudged) + ApplyResult(r => r.Type = HitResult.Perfect); - protected void EndHold() - { - holdStartTime = null; - bodyPiece.Hitting = false; + if (Tail.Result.Type == HitResult.Miss) + HasBroken = true; } public bool OnPressed(ManiaAction action) { - // Make sure the action happened within the body of the hold note - if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) + if (AllJudged) return false; if (action != Action.Value) return false; - // The user has pressed during the body of the hold note, after the head note and its hit windows have passed - // and within the limited range of the above if-statement. This state will be managed by the head note if the - // user has pressed during the hit windows of the head note. - BeginHold(); + beginHoldAt(Time.Current - Head.HitObject.StartTime); + Head.UpdateResult(); + return true; } + private void beginHoldAt(double timeOffset) + { + if (timeOffset < -Head.HitObject.HitWindows.WindowFor(HitResult.Miss)) + return; + + HoldStartTime = Time.Current; + bodyPiece.Hitting = true; + } + public bool OnReleased(ManiaAction action) { - // Make sure that the user started holding the key during the hold note - if (!holdStartTime.HasValue) + if (AllJudged) return false; if (action != Action.Value) return false; - EndHold(); + // Make sure a hold was started + if (HoldStartTime == null) + return false; + + Tail.UpdateResult(); + endHold(); // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) - hasBroken = true; + HasBroken = true; return true; } - /// - /// The head note of a hold. - /// - private class DrawableHeadNote : DrawableNote + private void endHold() { - private readonly DrawableHoldNote holdNote; - - public DrawableHeadNote(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Head) - { - this.holdNote = holdNote; - } - - public override bool OnPressed(ManiaAction action) - { - if (!base.OnPressed(action)) - return false; - - // If the key has been released too early, the user should not receive full score for the release - if (Result.Type == HitResult.Miss) - holdNote.hasBroken = true; - - // The head note also handles early hits before the body, but we want accurate early hits to count as the body being held - // The body doesn't handle these early early hits, so we have to explicitly set the holding state here - holdNote.BeginHold(); - - return true; - } - } - - /// - /// The tail note of a hold. - /// - private class DrawableTailNote : DrawableNote - { - /// - /// Lenience of release hit windows. This is to make cases where the hold note release - /// is timed alongside presses of other hit objects less awkward. - /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps - /// - private const double release_window_lenience = 1.5; - - private readonly DrawableHoldNote holdNote; - - public DrawableTailNote(DrawableHoldNote holdNote) - : base(holdNote.HitObject.Tail) - { - this.holdNote = holdNote; - } - - protected override void CheckForResult(bool userTriggered, double timeOffset) - { - Debug.Assert(HitObject.HitWindows != null); - - // Factor in the release lenience - timeOffset /= release_window_lenience; - - if (!userTriggered) - { - if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); - - return; - } - - var result = HitObject.HitWindows.ResultFor(timeOffset); - if (result == HitResult.None) - return; - - ApplyResult(r => - { - if (holdNote.hasBroken && (result == HitResult.Perfect || result == HitResult.Perfect)) - result = HitResult.Good; - - r.Type = result; - }); - } - - public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down - - public override bool OnReleased(ManiaAction action) - { - // Make sure that the user started holding the key during the hold note - if (!holdNote.holdStartTime.HasValue) - return false; - - if (action != Action.Value) - return false; - - UpdateResult(true); - - // Handled by the hold note, which will set holding = false - return false; - } + HoldStartTime = null; + bodyPiece.Hitting = false; } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs new file mode 100644 index 0000000000..a5d03bf765 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -0,0 +1,22 @@ +// 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.Rulesets.Mania.Objects.Drawables +{ + /// + /// The head of a . + /// + public class DrawableHoldNoteHead : DrawableNote + { + public DrawableHoldNoteHead(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Head) + { + } + + public void UpdateResult() => base.UpdateResult(true); + + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note + + public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs new file mode 100644 index 0000000000..a660144dd1 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mania.Objects.Drawables +{ + /// + /// The tail of a . + /// + public class DrawableHoldNoteTail : DrawableNote + { + /// + /// Lenience of release hit windows. This is to make cases where the hold note release + /// is timed alongside presses of other hit objects less awkward. + /// Todo: This shouldn't exist for non-LegacyBeatmapDecoder beatmaps + /// + private const double release_window_lenience = 1.5; + + private readonly DrawableHoldNote holdNote; + + public DrawableHoldNoteTail(DrawableHoldNote holdNote) + : base(holdNote.HitObject.Tail) + { + this.holdNote = holdNote; + } + + public void UpdateResult() => base.UpdateResult(true); + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + Debug.Assert(HitObject.HitWindows != null); + + // Factor in the release lenience + timeOffset /= release_window_lenience; + + if (!userTriggered) + { + if (!HitObject.HitWindows.CanBeHit(timeOffset)) + ApplyResult(r => r.Type = HitResult.Miss); + + return; + } + + var result = HitObject.HitWindows.ResultFor(timeOffset); + if (result == HitResult.None) + return; + + ApplyResult(r => + { + // If the head wasn't hit or the hold note was broken, cap the max score to Meh. + if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken)) + result = HitResult.Meh; + + r.Type = result; + }); + } + + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note + + public override bool OnReleased(ManiaAction action) => false; // Handled by the hold note + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs deleted file mode 100644 index 1d25a0c966..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/GlowPiece.cs +++ /dev/null @@ -1,68 +0,0 @@ -// 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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces -{ - public class GlowPiece : CompositeDrawable, IHasAccentColour - { - private const float glow_alpha = 0.7f; - private const float glow_radius = 5; - - public GlowPiece() - { - RelativeSizeAxes = Axes.Both; - Masking = true; - - InternalChild = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateGlow(); - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - if (accentColour == value) - return; - - accentColour = value; - - updateGlow(); - } - } - - private void updateGlow() - { - if (!IsLoaded) - return; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = AccentColour.Opacity(glow_alpha), - Radius = glow_radius, - Hollow = true - }; - } - } -} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs deleted file mode 100644 index 48c7ea7b7f..0000000000 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/LaneGlowPiece.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osuTK.Graphics; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; - -namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces -{ - public class LaneGlowPiece : CompositeDrawable, IHasAccentColour - { - private const float total_height = 100; - private const float glow_height = 50; - private const float glow_alpha = 0.4f; - private const float edge_alpha = 0.3f; - - public LaneGlowPiece() - { - BypassAutoSizeAxes = Axes.Both; - RelativeSizeAxes = Axes.X; - Height = total_height; - - InternalChildren = new[] - { - new Container - { - Name = "Left edge", - RelativeSizeAxes = Axes.Y, - Width = 1, - Children = createGradient(edge_alpha) - }, - new Container - { - Name = "Right edge", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 1, - Children = createGradient(edge_alpha) - }, - new Container - { - Name = "Glow", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = glow_height, - Children = createGradient(glow_alpha) - } - }; - } - - private Drawable[] createGradient(float alpha) => new Drawable[] - { - new Box - { - Name = "Top", - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientVertical(Color4.Transparent, Color4.White.Opacity(alpha)) - }, - new Box - { - Name = "Bottom", - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Blending = BlendingParameters.Additive, - Colour = ColourInfo.GradientVertical(Color4.White.Opacity(alpha), Color4.Transparent) - } - }; - - public Color4 AccentColour - { - get => Colour; - set => Colour = value; - } - } -} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index 70ba5cd938..877a9ee410 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Replays public void ConvertFrom(LegacyReplayFrame legacyFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) { // We don't need to fully convert, just create the converter - var converter = new ManiaBeatmapConverter(beatmap); + var converter = new ManiaBeatmapConverter(beatmap, new ManiaRuleset()); // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index a678ef60e7..9b54b48de3 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -1,87 +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.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_min = 0.75; - - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_mid = 0.85; - - /// - /// The hit HP multiplier at OD = 0. - /// - private const double hp_multiplier_max = 1; - - /// - /// The MISS HP multiplier at OD = 0. - /// - private const double hp_multiplier_miss_min = 0.5; - - /// - /// The MISS HP multiplier at OD = 5. - /// - private const double hp_multiplier_miss_mid = 0.75; - - /// - /// The MISS HP multiplier at OD = 10. - /// - private const double hp_multiplier_miss_max = 1; - - /// - /// The MISS HP multiplier. This is multiplied to the miss hp increase. - /// - private double hpMissMultiplier = 1; - - /// - /// The HIT HP multiplier. This is multiplied to hit hp increases. - /// - private double hpMultiplier = 1; - - public ManiaScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - BeatmapDifficulty difficulty = beatmap.BeatmapInfo.BaseDifficulty; - hpMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_min, hp_multiplier_mid, hp_multiplier_max); - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(difficulty.DrainRate, hp_multiplier_miss_min, hp_multiplier_miss_mid, hp_multiplier_miss_max); - } - - protected override void SimulateAutoplay(IBeatmap beatmap) - { - while (true) - { - base.SimulateAutoplay(beatmap); - - if (!HasFailed) - break; - - hpMultiplier *= 1.01; - hpMissMultiplier *= 0.98; - - Reset(false); - } - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier; - public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs new file mode 100644 index 0000000000..f3739ce7c2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -0,0 +1,67 @@ +// 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.Textures; +using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Scoring; +using osu.Game.Audio; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class ManiaLegacySkinTransformer : ISkin + { + private readonly ISkin source; + + public ManiaLegacySkinTransformer(ISkin source) + { + this.source = source; + } + + public Drawable GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case GameplaySkinComponent resultComponent: + return getResult(resultComponent); + } + + return null; + } + + private Drawable getResult(GameplaySkinComponent resultComponent) + { + switch (resultComponent.Component) + { + case HitResult.Miss: + return this.GetAnimation("mania-hit0", true, true); + + case HitResult.Meh: + return this.GetAnimation("mania-hit50", true, true); + + case HitResult.Ok: + return this.GetAnimation("mania-hit100", true, true); + + case HitResult.Good: + return this.GetAnimation("mania-hit200", true, true); + + case HitResult.Great: + return this.GetAnimation("mania-hit300", true, true); + + case HitResult.Perfect: + return this.GetAnimation("mania-hit300g", true, true); + } + + return null; + } + + public Texture GetTexture(string componentName) => source.GetTexture(componentName); + + public SampleChannel GetSample(ISampleInfo sample) => source.GetSample(sample); + + public IBindable GetConfig(TLookup lookup) => + source.GetConfig(lookup); + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs index 386bcbb724..ee2cec1bbd 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs @@ -22,26 +22,15 @@ namespace osu.Game.Rulesets.Mania.UI.Components private readonly IBindable direction = new Bindable(); - private readonly Container hitTargetLine; - private readonly Drawable hitTargetBar; + private readonly Drawable hitTarget; public ColumnHitObjectArea(HitObjectContainer hitObjectContainer) { InternalChildren = new[] { - hitTargetBar = new Box + hitTarget = new DefaultHitTarget { RelativeSizeAxes = Axes.X, - Height = NotePiece.NOTE_HEIGHT, - Alpha = 0.6f, - Colour = Color4.Black - }, - hitTargetLine = new Container - { - RelativeSizeAxes = Axes.X, - Height = hit_target_bar_height, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } }, hitObjectContainer }; @@ -55,17 +44,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components { Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - hitTargetBar.Anchor = hitTargetBar.Origin = anchor; - hitTargetLine.Anchor = hitTargetLine.Origin = anchor; + hitTarget.Anchor = hitTarget.Origin = anchor; }, true); } - protected override void LoadComplete() - { - base.LoadComplete(); - updateColours(); - } - private Color4 accentColour; public Color4 AccentColour @@ -78,21 +60,86 @@ namespace osu.Game.Rulesets.Mania.UI.Components accentColour = value; - updateColours(); + if (hitTarget is IHasAccentColour colouredHitTarget) + colouredHitTarget.AccentColour = accentColour; } } - private void updateColours() + private class DefaultHitTarget : CompositeDrawable, IHasAccentColour { - if (!IsLoaded) - return; + private readonly IBindable direction = new Bindable(); - hitTargetLine.EdgeEffect = new EdgeEffectParameters + private readonly Container hitTargetLine; + private readonly Drawable hitTargetBar; + + public DefaultHitTarget() { - Type = EdgeEffectType.Glow, - Radius = 5, - Colour = accentColour.Opacity(0.5f), - }; + InternalChildren = new[] + { + hitTargetBar = new Box + { + RelativeSizeAxes = Axes.X, + Height = NotePiece.NOTE_HEIGHT, + Alpha = 0.6f, + Colour = Color4.Black + }, + hitTargetLine = new Container + { + RelativeSizeAxes = Axes.X, + Height = hit_target_bar_height, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + }, + }; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(dir => + { + Anchor anchor = dir.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + + hitTargetBar.Anchor = hitTargetBar.Origin = anchor; + hitTargetLine.Anchor = hitTargetLine.Origin = anchor; + }, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateColours(); + } + + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + if (accentColour == value) + return; + + accentColour = value; + + updateColours(); + } + } + + private void updateColours() + { + if (!IsLoaded) + return; + + hitTargetLine.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 5, + Colour = accentColour.Opacity(0.5f), + }; + } } } } diff --git a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs index 2fad3bd04f..d4c3000d3f 100644 --- a/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/SkinnableTestScene.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests [BackgroundDependencyLoader] private void load(AudioManager audio, SkinManager skinManager) { - var dllStore = new DllResourceStore("osu.Game.Rulesets.Osu.Tests.dll"); + var dllStore = new DllResourceStore(typeof(SkinnableTestScene).Assembly); metricsSkin = new TestLegacySkin(new SkinInfo(), new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); defaultSkin = skinManager.GetSkin(DefaultLegacySkin.Info); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs index eff4d919b0..c9b3d08a22 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs @@ -33,8 +33,8 @@ namespace osu.Game.Rulesets.Osu.Tests typeof(CircularDistanceSnapGrid) }; - [Cached(typeof(IEditorBeatmap))] - private readonly EditorBeatmap editorBeatmap; + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap; [Cached] private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneOsuDistanceSnapGrid() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(new OsuBeatmap()); } [SetUp] diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 02ce77e707..bd9d948782 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Tests EndTime = 6000, }, // placeholder object to avoid hitting the results screen - new HitObject + new HitCircle { StartTime = 99999, } diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj index 9d4e016eae..1d8c4708c1 100644 --- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj +++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index 2296030f81..147d74c929 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using System.Collections.Generic; using osu.Game.Rulesets.Objects.Types; -using System; +using System.Linq; using osu.Game.Rulesets.Osu.UI; using osu.Framework.Extensions.IEnumerableExtensions; @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { public class OsuBeatmapConverter : BeatmapConverter { - public OsuBeatmapConverter(IBeatmap beatmap) - : base(beatmap) + public OsuBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) { } - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasPosition) }; + public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index a2c1a5f5f4..49624ea733 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -91,10 +91,10 @@ namespace osu.Game.Rulesets.Osu.Edit if (sourceIndex == -1) return null; - OsuHitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; + HitObject sourceObject = EditorBeatmap.HitObjects[sourceIndex]; int targetIndex = sourceIndex + targetOffset; - OsuHitObject targetObject = null; + HitObject targetObject = null; // Keep advancing the target object while its start time falls before the end time of the source object while (true) @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Edit targetIndex++; } - return new OsuDistanceSnapGrid(sourceObject, targetObject); + return new OsuDistanceSnapGrid((OsuHitObject)sourceObject, (OsuHitObject)targetObject); } } } diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index 7a5b98864c..bf30fbc351 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -27,22 +27,5 @@ namespace osu.Game.Rulesets.Osu.Judgements return 300; } } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - case HitResult.Miss: - return -0.02; - - case HitResult.Meh: - case HitResult.Good: - case HitResult.Great: - return 0.01; - - default: - return 0; - } - } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 63110b2797..831e4a700f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -18,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToScoreProcessor + public class OsuModBlinds : Mod, IApplicableToDrawableRuleset, IApplicableToHealthProcessor { public override string Name => "Blinds"; public override string Description => "Play with blinds on your screen."; @@ -37,9 +37,9 @@ namespace osu.Game.Rulesets.Osu.Mods drawableRuleset.Overlays.Add(blinds = new DrawableOsuBlinds(drawableRuleset.Playfield.HitObjectContainer, drawableRuleset.Beatmap)); } - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { - scoreProcessor.Health.ValueChanged += health => { blinds.AnimateClosedness((float)health.NewValue); }; + healthProcessor.Health.ValueChanged += health => { blinds.AnimateClosedness((float)health.NewValue); }; } public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index adca95cf8a..9bf7525d33 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -5,7 +5,7 @@ using osu.Framework.Graphics.Sprites; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModDeflate : OsuModeObjectScaleTween + public class OsuModDeflate : OsuModObjectScaleTween { public override string Name => "Deflate"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs new file mode 100644 index 0000000000..7eee71be81 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -0,0 +1,49 @@ +// 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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModDifficultyAdjust : ModDifficultyAdjust + { + [SettingSource("Circle Size", "Override a beatmap's set CS.")] + public BindableNumber CircleSize { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Approach Rate", "Override a beatmap's set AR.")] + public BindableNumber ApproachRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + protected override void TransferSettings(BeatmapDifficulty difficulty) + { + base.TransferSettings(difficulty); + + TransferSetting(CircleSize, difficulty.CircleSize); + TransferSetting(ApproachRate, difficulty.ApproachRate); + } + + protected override void ApplySettings(BeatmapDifficulty difficulty) + { + base.ApplySettings(difficulty); + + difficulty.CircleSize = CircleSize.Value; + difficulty.ApproachRate = ApproachRate.Value; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index 3c81203ad7..76676ce888 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -5,7 +5,7 @@ using osu.Framework.Graphics.Sprites; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModGrow : OsuModeObjectScaleTween + internal class OsuModGrow : OsuModObjectScaleTween { public override string Name => "Grow"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs similarity index 96% rename from osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs rename to osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 923278f484..42ddddc4dd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModeObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Adjusts the size of hit objects during their fade in animation. /// - public abstract class OsuModeObjectScaleTween : Mod, IReadFromConfig, IApplicableToDrawableHitObjects + public abstract class OsuModObjectScaleTween : Mod, IReadFromConfig, IApplicableToDrawableHitObjects { public override ModType Type => ModType.Fun; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index e786ec86f9..eae218509e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; // todo: this mod should be able to be compatible with hidden with a bit of further implementation. - public override Type[] IncompatibleMods => new[] { typeof(OsuModeObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModObjectScaleTween), typeof(OsuModHidden), typeof(OsuModTraceable) }; private const int rotate_offset = 360; private const float rotate_starting_width = 2; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index cf1ce517e8..dff9a77807 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModeObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; private Bindable increaseFirstObjectVisibility = new Bindable(); public void ReadFromConfig(OsuConfigManager config) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 27af615935..148869f5e8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -32,13 +32,13 @@ using System; namespace osu.Game.Rulesets.Osu { - public class OsuRuleset : Ruleset + public class OsuRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableOsuRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new OsuScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new OsuBeatmapConverter(beatmap, this); public override IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => new OsuBeatmapProcessor(beatmap); @@ -130,6 +130,7 @@ namespace osu.Game.Rulesets.Osu return new Mod[] { new OsuModTarget(), + new OsuModDifficultyAdjust(), }; case ModType.Automation: @@ -174,11 +175,13 @@ namespace osu.Game.Rulesets.Osu public override string ShortName => SHORT_NAME; + public override string PlayingVerb => "Clicking circles"; + public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this); public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkinTransformer(source); - public override int? LegacyID => 0; + public int LegacyID => 0; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 6779271cb3..1de7d488f3 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,7 +1,6 @@ // 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.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -11,44 +10,6 @@ namespace osu.Game.Rulesets.Osu.Scoring { internal class OsuScoreProcessor : ScoreProcessor { - public OsuScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - private float hpDrainRate; - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpDrainRate = beatmap.BeatmapInfo.BaseDifficulty.DrainRate; - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - { - switch (result.Type) - { - case HitResult.Great: - return 10.2 - hpDrainRate; - - case HitResult.Good: - return 8 - hpDrainRate; - - case HitResult.Meh: - return 4 - hpDrainRate; - - // case HitResult.SliderTick: - // return Math.Max(7 - hpDrainRate, 0) * 0.01; - - case HitResult.Miss: - return hpDrainRate; - - default: - return 0; - } - } - protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); public override HitWindows CreateHitWindows() => new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj index d728d65bfd..da89b37fbf 100644 --- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj +++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs index aaf113f216..c31b07344d 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs @@ -26,10 +26,6 @@ namespace osu.Game.Rulesets.Taiko.Audio var centre = s.GetSampleInfo(); var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP); - // todo: this is ugly - centre.Namespace = "taiko"; - rim.Namespace = "taiko"; - mappings[s.Time] = new DrumSample { Centre = addSound(centre), diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 10cc861b7e..cc9d6e4470 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -39,14 +39,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps private readonly bool isForCurrentRuleset; - protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; - - public TaikoBeatmapConverter(IBeatmap beatmap) - : base(beatmap) + public TaikoBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) { - isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(new TaikoRuleset().RulesetInfo); + isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); } + public override bool CanConvert() => true; + protected override Beatmap ConvertBeatmap(IBeatmap original) { // Rewrite the beatmap info to add the slider velocity multiplier diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs new file mode 100644 index 0000000000..56a73ad7df --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -0,0 +1,11 @@ +// 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.Mods; + +namespace osu.Game.Rulesets.Taiko.Mods +{ + public class TaikoModDifficultyAdjust : ModDifficultyAdjust + { + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 2da5a9c403..b9d31ff906 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -166,8 +166,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables // Normal and clap samples are handled by the drum protected override IEnumerable GetSamples() => HitObject.Samples.Where(s => s.Name != HitSampleInfo.HIT_NORMAL && s.Name != HitSampleInfo.HIT_CLAP); - protected override string SampleNamespace => "taiko"; - protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); /// diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 4b234b56d4..48eb33976e 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -121,41 +121,13 @@ namespace osu.Game.Rulesets.Taiko.Replays var nextHitObject = GetNextObject(i); // Get the next object that requires pressing the same button bool canDelayKeyUp = nextHitObject == null || nextHitObject.StartTime > endTime + KEY_UP_DELAY; - double calculatedDelay = canDelayKeyUp ? KEY_UP_DELAY : (nextHitObject.StartTime - endTime) * 0.9; - Frames.Add(new TaikoReplayFrame(endTime + calculatedDelay)); - if (i < Beatmap.HitObjects.Count - 1) - { - double waitTime = Beatmap.HitObjects[i + 1].StartTime - 1000; - if (waitTime > endTime) - Frames.Add(new TaikoReplayFrame(waitTime)); - } - hitButton = !hitButton; } return Replay; } - - protected override HitObject GetNextObject(int currentIndex) - { - Type desiredType = Beatmap.HitObjects[currentIndex].GetType(); - - for (int i = currentIndex + 1; i < Beatmap.HitObjects.Count; i++) - { - var currentObj = Beatmap.HitObjects[i]; - - if (currentObj.GetType() == desiredType || - // Un-press all keys before a DrumRoll or Swell - currentObj is DrumRoll || currentObj is Swell) - { - return Beatmap.HitObjects[i]; - } - } - - return null; - } } } diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitclap.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitclap.wav new file mode 100755 index 0000000000..9ea2be5855 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitclap.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitfinish.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitfinish.wav new file mode 100755 index 0000000000..af270ae12a Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitfinish.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitnormal.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitnormal.wav new file mode 100755 index 0000000000..3d8024c6ae Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitnormal.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitwhistle.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitwhistle.wav new file mode 100755 index 0000000000..16d254cc87 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/normal-hitwhistle.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitclap.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitclap.wav new file mode 100755 index 0000000000..b4cfa26265 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitclap.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitfinish.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitfinish.wav new file mode 100755 index 0000000000..97804a5a61 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitfinish.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitnormal.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitnormal.wav new file mode 100755 index 0000000000..67f02877a8 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitnormal.wav differ diff --git a/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitwhistle.wav b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitwhistle.wav new file mode 100755 index 0000000000..10206cd228 Binary files /dev/null and b/osu.Game.Rulesets.Taiko/Resources/Samples/Gameplay/soft-hitwhistle.wav differ diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs new file mode 100644 index 0000000000..edb089dbac --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -0,0 +1,49 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Scoring +{ + /// + /// A for the taiko ruleset. + /// Taiko fails if the player has not half-filled their health by the end of the map. + /// + public class TaikoHealthProcessor : AccumulatingHealthProcessor + { + /// + /// A value used for calculating . + /// + private const double object_count_factor = 3; + + /// + /// HP multiplier for a successful . + /// + private double hpMultiplier; + + /// + /// HP multiplier for a . + /// + private double hpMissMultiplier; + + public TaikoHealthProcessor() + : base(0.5) + { + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + base.ApplyBeatmap(beatmap); + + hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); + hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); + } + + protected override double GetHealthIncreaseFor(JudgementResult result) + => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); + } +} diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index ae593d2e3a..003d40af56 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -1,60 +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 System.Linq; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Scoring { internal class TaikoScoreProcessor : ScoreProcessor { - /// - /// A value used for calculating . - /// - private const double object_count_factor = 3; - - /// - /// Taiko fails at the end of the map if the player has not half-filled their HP bar. - /// - protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; - - /// - /// HP multiplier for a successful . - /// - private double hpMultiplier; - - /// - /// HP multiplier for a . - /// - private double hpMissMultiplier; - - public TaikoScoreProcessor(IBeatmap beatmap) - : base(beatmap) - { - } - - protected override void ApplyBeatmap(IBeatmap beatmap) - { - base.ApplyBeatmap(beatmap); - - hpMultiplier = 1 / (object_count_factor * beatmap.HitObjects.OfType().Count() * BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.5, 0.75, 0.98)); - - hpMissMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, 0.0018, 0.0075, 0.0120); - } - - protected override double HealthAdjustmentFactorFor(JudgementResult result) - => result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier; - - protected override void Reset(bool storeResults) - { - base.Reset(storeResults); - - Health.Value = 0; - } - public override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs new file mode 100644 index 0000000000..381cd14cd4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -0,0 +1,55 @@ +// 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 osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Audio; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Skinning +{ + public class TaikoLegacySkinTransformer : ISkin + { + private readonly ISkinSource source; + + public TaikoLegacySkinTransformer(ISkinSource source) + { + this.source = source; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => source.GetDrawableComponent(component); + + public Texture GetTexture(string componentName) => source.GetTexture(componentName); + + public SampleChannel GetSample(ISampleInfo sampleInfo) => source.GetSample(new LegacyTaikoSampleInfo(sampleInfo)); + + public IBindable GetConfig(TLookup lookup) => source.GetConfig(lookup); + + private class LegacyTaikoSampleInfo : ISampleInfo + { + private readonly ISampleInfo source; + + public LegacyTaikoSampleInfo(ISampleInfo source) + { + this.source = source; + } + + public IEnumerable LookupNames + { + get + { + foreach (var name in source.LookupNames) + yield return $"taiko-{name}"; + + foreach (var name in source.LookupNames) + yield return name; + } + } + + public int Volume => source.Volume; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index ca7ab30867..fc79e59864 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -21,16 +21,22 @@ using osu.Game.Rulesets.Taiko.Difficulty; using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Scoring; using System; +using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko { - public class TaikoRuleset : Ruleset + public class TaikoRuleset : Ruleset, ILegacyRuleset { public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new DrawableTaikoRuleset(this, beatmap, mods); - public override ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new TaikoScoreProcessor(beatmap); + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap); + public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new TaikoHealthProcessor(); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TaikoBeatmapConverter(beatmap, this); + + public override ISkin CreateLegacySkinProvider(ISkinSource source) => new TaikoLegacySkinTransformer(source); public const string SHORT_NAME = "taiko"; @@ -105,6 +111,12 @@ namespace osu.Game.Rulesets.Taiko new TaikoModFlashlight(), }; + case ModType.Conversion: + return new Mod[] + { + new TaikoModDifficultyAdjust(), + }; + case ModType.Automation: return new Mod[] { @@ -127,13 +139,15 @@ namespace osu.Game.Rulesets.Taiko public override string ShortName => SHORT_NAME; + public override string PlayingVerb => "Bashing drums"; + public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetTaiko }; public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score); - public override int? LegacyID => 1; + public int LegacyID => 1; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); } diff --git a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs index 98e630abd2..12d729d09f 100644 --- a/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/EditorBeatmapTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestHitObjectAddEvent() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); HitObject addedObject = null; editorBeatmap.HitObjectAdded += h => addedObject = h; @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Beatmaps public void HitObjectRemoveEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject removedObject = null; editorBeatmap.HitObjectRemoved += h => removedObject = h; @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Beatmaps public void TestInitialHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject changedObject = null; editorBeatmap.StartTimeChanged += h => changedObject = h; @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestAddedHitObjectStartTimeChangeEvent() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); HitObject changedObject = null; editorBeatmap.StartTimeChanged += h => changedObject = h; @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Beatmaps public void TestRemovedHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); HitObject changedObject = null; editorBeatmap.StartTimeChanged += h => changedObject = h; @@ -110,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestAddHitObjectInMiddle() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Beatmaps public void TestResortWhenStartTimeChanged() { var hitCircle = new HitCircle { StartTime = 1000 }; - var editorBeatmap = new EditorBeatmap(new OsuBeatmap + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 26e70f19e4..33f484a9aa 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; using osu.Game.IO; +using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -313,7 +314,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decoder.Decode(stream); - var converted = new OsuBeatmapConverter(beatmap).Convert(); + var converted = new OsuBeatmapConverter(beatmap, new OsuRuleset()).Convert(); new OsuBeatmapProcessor(converted).PreProcess(); new OsuBeatmapProcessor(converted).PostProcess(); @@ -336,7 +337,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = decoder.Decode(stream); - var converted = new CatchBeatmapConverter(beatmap).Convert(); + var converted = new CatchBeatmapConverter(beatmap, new CatchRuleset()).Convert(); new CatchBeatmapProcessor(converted).PreProcess(); new CatchBeatmapProcessor(converted).PostProcess(); diff --git a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs index fe3cc375ea..2d336bd19c 100644 --- a/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editor/TestSceneHitObjectComposerDistanceSnapping.cs @@ -2,11 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; @@ -17,6 +18,9 @@ namespace osu.Game.Tests.Editor { private TestHitObjectComposer composer; + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + [SetUp] public void Setup() => Schedule(() => { @@ -183,7 +187,7 @@ namespace osu.Game.Tests.Editor private class TestHitObjectComposer : OsuHitObjectComposer { - public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; + public new EditorBeatmap EditorBeatmap => base.EditorBeatmap; public TestHitObjectComposer() : base(new OsuRuleset()) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs new file mode 100644 index 0000000000..eec52669ff --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -0,0 +1,159 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.MathUtils; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + [HeadlessTest] + public class TestSceneDrainingHealthProcessor : OsuTestScene + { + private Bindable breakTime; + private HealthProcessor processor; + private ManualClock clock; + + [Test] + public void TestInitialHealthStartsAtOne() + { + createProcessor(createBeatmap(1000, 2000)); + + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedBeforeGameplayStart() + { + createProcessor(createBeatmap(1000, 2000)); + + setTime(100); + assertHealthEqualTo(1); + setTime(900); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedAfterGameplayEnd() + { + createProcessor(createBeatmap(1000, 2000)); + setTime(2001); // After the hitobjects + setHealth(1); // Reset the current health for assertions to take place + + setTime(2100); + assertHealthEqualTo(1); + setTime(3000); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthNotDrainedDuringBreak() + { + createProcessor(createBeatmap(0, 2000)); + setBreak(true); + + setTime(700); + assertHealthEqualTo(1); + setTime(900); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthDrainedDuringGameplay() + { + createProcessor(createBeatmap(0, 1000)); + + setTime(500); + assertHealthNotEqualTo(1); + } + + [Test] + public void TestHealthGainedAfterRewind() + { + createProcessor(createBeatmap(0, 1000)); + setTime(500); + + setTime(0); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthGainedOnHit() + { + Beatmap beatmap = createBeatmap(0, 1000); + + createProcessor(beatmap); + setTime(10); // Decrease health slightly + assertHealthNotEqualTo(1); + + AddStep("apply hit result", () => processor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + assertHealthEqualTo(1); + } + + [Test] + public void TestHealthRemovedOnRevert() + { + var beatmap = createBeatmap(0, 1000); + JudgementResult result = null; + + createProcessor(beatmap); + setTime(10); // Decrease health slightly + AddStep("apply hit result", () => processor.ApplyResult(result = new JudgementResult(beatmap.HitObjects[0], new Judgement()) { Type = HitResult.Perfect })); + + AddStep("revert hit result", () => processor.RevertResult(result)); + assertHealthNotEqualTo(1); + } + + private Beatmap createBeatmap(double startTime, double endTime) + { + var beatmap = new Beatmap + { + BeatmapInfo = { BaseDifficulty = { DrainRate = 5 } }, + }; + + for (double time = startTime; time <= endTime; time += 100) + beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = time }); + + return beatmap; + } + + private void createProcessor(Beatmap beatmap) => AddStep("create processor", () => + { + breakTime = new Bindable(); + + Child = processor = new DrainingHealthProcessor(beatmap.HitObjects[0].StartTime).With(d => + { + d.RelativeSizeAxes = Axes.Both; + d.Clock = new FramedClock(clock = new ManualClock()); + }); + + processor.IsBreakTime.BindTo(breakTime); + processor.ApplyBeatmap(beatmap); + }); + + private void setTime(double time) => AddStep($"set time = {time}", () => clock.CurrentTime = time); + + private void setHealth(double health) => AddStep($"set health = {health}", () => processor.Health.Value = health); + + private void setBreak(bool enabled) => AddStep($"{(enabled ? "enable" : "disable")} break", () => breakTime.Value = enabled); + + private void assertHealthEqualTo(double value) + => AddAssert($"health = {value}", () => Precision.AlmostEquals(value, processor.Health.Value, 0.0001f)); + + private void assertHealthNotEqualTo(double value) + => AddAssert($"health != {value}", () => !Precision.AlmostEquals(value, processor.Health.Value, 0.0001f)); + + private class JudgeableHitObject : HitObject + { + public override Judgement CreateJudgement() => new Judgement(); + } + } +} diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs new file mode 100644 index 0000000000..84506739ab --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -0,0 +1,76 @@ +// 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.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.IO.Stores; +using osu.Framework.Testing; +using osu.Game.Audio; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + [HeadlessTest] + public class TestSceneStoryboardSamples : OsuTestScene + { + [Test] + public void TestRetrieveTopLevelSample() + { + ISkin skin = null; + SampleChannel channel = null; + + AddStep("create skin", () => skin = new TestSkin("test-sample", Audio)); + AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample"))); + + AddAssert("sample is non-null", () => channel != null); + } + + [Test] + public void TestRetrieveSampleInSubFolder() + { + ISkin skin = null; + SampleChannel channel = null; + + AddStep("create skin", () => skin = new TestSkin("folder/test-sample", Audio)); + AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample"))); + + AddAssert("sample is non-null", () => channel != null); + } + + private class TestSkin : LegacySkin + { + public TestSkin(string resourceName, AudioManager audioManager) + : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") + { + } + } + + private class TestResourceStore : IResourceStore + { + private readonly string resourceName; + + public TestResourceStore(string resourceName) + { + this.resourceName = resourceName; + } + + public byte[] Get(string name) => name == resourceName ? TestResources.GetStore().Get("Resources/test-sample.mp3") : null; + + public Task GetAsync(string name) => name == resourceName ? TestResources.GetStore().GetAsync("Resources/test-sample.mp3") : null; + + public Stream GetStream(string name) => name == resourceName ? TestResources.GetStore().GetStream("Resources/test-sample.mp3") : null; + + public IEnumerable GetAvailableResources() => new[] { resourceName }; + + public void Dispose() + { + } + } + } +} diff --git a/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus.osz b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus.osz new file mode 100644 index 0000000000..987dbea6db Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus.osz differ diff --git a/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual.osz b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual.osz new file mode 100644 index 0000000000..8a92423d35 Binary files /dev/null and b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual.osz differ diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index a57405628a..8b892fbb2f 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -9,11 +9,11 @@ namespace osu.Game.Tests.Resources { public static class TestResources { - public static DllResourceStore GetStore() => new DllResourceStore("osu.Game.Tests.dll"); + public static DllResourceStore GetStore() => new DllResourceStore(typeof(TestResources).Assembly); 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"); + public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); public static string GetTestBeatmapForImport(bool virtualTrack = false) { diff --git a/osu.Game.Tests/Resources/test-sample.mp3 b/osu.Game.Tests/Resources/test-sample.mp3 new file mode 100644 index 0000000000..f7c344f39a Binary files /dev/null and b/osu.Game.Tests/Resources/test-sample.mp3 differ diff --git a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs index 9f16e1d781..3562689482 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneComposeScreen.cs @@ -4,6 +4,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose; namespace osu.Game.Tests.Visual.Editor @@ -11,10 +13,21 @@ namespace osu.Game.Tests.Visual.Editor [TestFixture] public class TestSceneComposeScreen : EditorClockTestScene { + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap = + new EditorBeatmap(new OsuBeatmap + { + BeatmapInfo = + { + Ruleset = new OsuRuleset().RulesetInfo + } + }); + [BackgroundDependencyLoader] private void load() { - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Child = new ComposeScreen(); } } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs index 39b4bf7218..847d168e51 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneDistanceSnapGrid.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Beatmaps; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,15 +20,15 @@ namespace osu.Game.Tests.Visual.Editor private const double beat_length = 100; private static readonly Vector2 grid_position = new Vector2(512, 384); - [Cached(typeof(IEditorBeatmap))] - private readonly EditorBeatmap editorBeatmap; + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap; [Cached(typeof(IDistanceSnapProvider))] private readonly SnapProvider snapProvider = new SnapProvider(); public TestSceneDistanceSnapGrid() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length }); } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs index ed6bc5fe0c..29575cb42e 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneEditorComposeTimeline.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Editor { Beatmap.Value = new WaveformTestBeatmap(audio); - var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap); + var editorBeatmap = new EditorBeatmap((Beatmap)Beatmap.Value.Beatmap); Children = new Drawable[] { diff --git a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs index b7c7028b52..c001c83877 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneHitObjectComposer.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -59,9 +60,12 @@ namespace osu.Game.Tests.Visual.Editor }, }); + var editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo)); + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; Dependencies.CacheAs(clock); Dependencies.CacheAs(clock); + Dependencies.CacheAs(editorBeatmap); Child = new OsuHitObjectComposer(new OsuRuleset()); } diff --git a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs index 121853d8d0..adfed9a299 100644 --- a/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editor/TestSceneTimingScreen.cs @@ -5,7 +5,8 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; namespace osu.Game.Tests.Visual.Editor @@ -25,10 +26,13 @@ namespace osu.Game.Tests.Visual.Editor typeof(RowAttribute) }; + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + [BackgroundDependencyLoader] private void load() { - Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); Child = new TimingScreen(); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index ae20bbc86d..36235a4418 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawableScrollingRuleset(this, beatmap, mods); - public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap); + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, null); public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); @@ -268,12 +268,12 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestBeatmapConverter : BeatmapConverter { - public TestBeatmapConverter(IBeatmap beatmap) - : base(beatmap) + public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) { } - protected override IEnumerable ValidConversionTypes => new[] { typeof(HitObject) }; + public override bool CanConvert() => true; protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs index 992c47f856..81050b1637 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailAnimation.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void LoadComplete() { base.LoadComplete(); - ScoreProcessor.FailConditions += (_, __) => true; + HealthProcessor.FailConditions += (_, __) => true; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index 1580aac8c5..2045072c79 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -22,12 +22,12 @@ namespace osu.Game.Tests.Visual.Gameplay { AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); - AddAssert("total judgements == 1", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits == 1); + AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.JudgedHits >= 1); } private class FailPlayer : TestPlayer { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + public new HealthProcessor HealthProcessor => base.HealthProcessor; public FailPlayer() : base(false, false) @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void LoadComplete() { base.LoadComplete(); - ScoreProcessor.FailConditions += (_, __) => true; + HealthProcessor.FailConditions += (_, __) => true; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 39c42980ab..ee58219cd3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create overlay", () => { - Child = hudOverlay = new HUDOverlay(null, null, Array.Empty()); + Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); action?.Invoke(hudOverlay); }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index e04315894e..1a83e35e4f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestResumeWithResumeOverlay() { AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1); + AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1); pauseAndConfirm(); resume(); @@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestPauseWithResumeOverlay() { AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1); + AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1); pauseAndConfirm(); @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("move cursor to button", () => InputManager.MoveMouseTo(Player.HUDOverlay.HoldToQuit.Children.OfType().First().ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for hitobjects", () => Player.ScoreProcessor.Health.Value < 1); + AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1); pauseAndConfirm(); resumeAndConfirm(); @@ -285,7 +285,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected class PausePlayer : TestPlayer { - public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + public new HealthProcessor HealthProcessor => base.HealthProcessor; public new HUDOverlay HUDOverlay => base.HUDOverlay; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index aa80819694..8629522dc2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -28,12 +30,16 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(IReadOnlyList))] private IReadOnlyList mods { get; set; } = Array.Empty(); + private const int spawn_interval = 5000; + private readonly ScrollingTestContainer[] scrollContainers = new ScrollingTestContainer[4]; private readonly TestPlayfield[] playfields = new TestPlayfield[4]; + private ScheduledDelegate hitObjectSpawnDelegate; - public TestSceneScrollingHitObjects() + [SetUp] + public void Setup() => Schedule(() => { - Add(new GridContainer + Child = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -43,48 +49,66 @@ namespace osu.Game.Tests.Visual.Gameplay scrollContainers[0] = new ScrollingTestContainer(ScrollingDirection.Up) { RelativeSizeAxes = Axes.Both, - Child = playfields[0] = new TestPlayfield() + Child = playfields[0] = new TestPlayfield(), + TimeRange = spawn_interval }, - scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[1] = new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both, - Child = playfields[1] = new TestPlayfield() + Child = playfields[1] = new TestPlayfield(), + TimeRange = spawn_interval }, }, new Drawable[] { - scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[2] = new ScrollingTestContainer(ScrollingDirection.Left) { RelativeSizeAxes = Axes.Both, - Child = playfields[2] = new TestPlayfield() + Child = playfields[2] = new TestPlayfield(), + TimeRange = spawn_interval }, - scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Up) + scrollContainers[3] = new ScrollingTestContainer(ScrollingDirection.Right) { RelativeSizeAxes = Axes.Both, - Child = playfields[3] = new TestPlayfield() + Child = playfields[3] = new TestPlayfield(), + TimeRange = spawn_interval } } } - }); + }; + setUpHitObjects(); + }); + + private void setUpHitObjects() + { + scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); + + for (int i = 0; i <= spawn_interval; i += 1000) + addHitObject(Time.Current + i); + + hitObjectSpawnDelegate?.Cancel(); + hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + spawn_interval), 1000, true); + } + + [Test] + public void TestScrollAlgorithms() + { AddStep("Constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); AddStep("Overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping)); AddStep("Sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential)); - AddSliderStep("Time range", 100, 10000, 5000, v => scrollContainers.ForEach(c => c.TimeRange = v)); - AddStep("Add control point", () => addControlPoint(Time.Current + 5000)); + AddSliderStep("Time range", 100, 10000, spawn_interval, v => scrollContainers.Where(c => c != null).ForEach(c => c.TimeRange = v)); + AddStep("Add control point", () => addControlPoint(Time.Current + spawn_interval)); } - protected override void LoadComplete() + [Test] + public void TestScrollLifetime() { - base.LoadComplete(); - - scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0))); - - for (int i = 0; i <= 5000; i += 1000) - addHitObject(Time.Current + i); - - Scheduler.AddDelayed(() => addHitObject(Time.Current + 5000), 1000, true); + AddStep("Set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant)); + // scroll container time range must be less than the rate of spawning hitobjects + // otherwise the hitobjects will spawn already partly visible on screen and look wrong + AddStep("Set time range", () => scrollContainers.ForEach(c => c.TimeRange = spawn_interval / 2.0)); } private void addHitObject(double time) @@ -207,7 +231,9 @@ namespace osu.Game.Tests.Visual.Gameplay public TestDrawableHitObject(double time) : base(new HitObject { StartTime = time }) { - Origin = Anchor.Centre; + Origin = Anchor.Custom; + OriginPosition = new Vector2(75 / 4.0f); + AutoSizeAxes = Axes.Both; AddInternal(new Box { Size = new Vector2(75) }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs index 63b8acb234..63b46c991f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileHeader.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online typeof(ProfileHeader), typeof(RankGraph), typeof(LineGraph), - typeof(OverlayHeaderTabControl), + typeof(TabControlOverlayHeader.OverlayHeaderTabControl), typeof(CentreHeaderContainer), typeof(BottomHeaderContainer), typeof(DetailHeaderContainer), diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs deleted file mode 100644 index 66144cbfe4..0000000000 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ /dev/null @@ -1,237 +0,0 @@ -// 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 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; - -namespace osu.Game.Tests.Visual.SongSelect -{ - [TestFixture] - [System.ComponentModel.Description("PlaySongSelect leaderboard/details area")] - public class TestSceneBeatmapDetailArea : OsuTestScene - { - public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; - - private ModDisplay modDisplay; - - [BackgroundDependencyLoader] - private void load(OsuGameBase game, RulesetStore rulesets) - { - BeatmapDetailArea detailsArea; - Add(detailsArea = new BeatmapDetailArea - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - 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 = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "All Metrics", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has all the metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 7, - DrainRate = 1, - OverallDifficulty = 5.7f, - ApproachRate = 3.5f, - }, - StarDifficulty = 5.3f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("all except source", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "All Metrics", - Metadata = new BeatmapMetadata - { - Tags = "this beatmap has all the metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 7, - DrainRate = 1, - OverallDifficulty = 5.7f, - ApproachRate = 3.5f, - }, - StarDifficulty = 5.3f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("ratings", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - BeatmapSet = new BeatmapSetInfo - { - Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() } - }, - Version = "Only Ratings", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has ratings metrics but not retries or fails", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 6, - DrainRate = 9, - OverallDifficulty = 6, - ApproachRate = 6, - }, - StarDifficulty = 4.8f - } - })); - - AddStep("fails+retries", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "Only Retries and Fails", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has retries and fails but no ratings", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 3.7f, - DrainRate = 6, - OverallDifficulty = 6, - ApproachRate = 7, - }, - StarDifficulty = 2.91f, - Metrics = new BeatmapMetrics - { - Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(), - Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(), - }, - } - })); - - AddStep("null metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap - { - BeatmapInfo = - { - Version = "No Metrics", - Metadata = new BeatmapMetadata - { - Source = "osu!lazer", - Tags = "this beatmap has no metrics", - }, - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 5, - DrainRate = 5, - OverallDifficulty = 5.5f, - ApproachRate = 6.5f, - }, - StarDifficulty = 1.97f, - } - })); - - 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.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs index acf037198f..6aa5a76490 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetails.cs @@ -3,8 +3,14 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.SongSelect @@ -174,5 +180,27 @@ namespace osu.Game.Tests.Visual.SongSelect OnlineBeatmapID = 162, }); } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private OsuColour colours { get; set; } + + [Test] + public void TestModAdjustments() + { + TestAllMetrics(); + + Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); + + AddStep("with EZ mod", () => SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }); + + AddAssert("first bar coloured blue", () => details.ChildrenOfType().Skip(1).First().AccentColour == colours.BlueDark); + + AddStep("with HR mod", () => SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }); + + AddAssert("first bar coloured red", () => details.ChildrenOfType().Skip(1).First().AccentColour == colours.Red); + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbs.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs similarity index 92% rename from osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbs.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs index 554696765e..19eebc89b6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbs.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBreadcrumbControl.cs @@ -10,11 +10,11 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneBreadcrumbs : OsuTestScene + public class TestSceneBreadcrumbControl : OsuTestScene { private readonly BreadcrumbControl breadcrumbs; - public TestSceneBreadcrumbs() + public TestSceneBreadcrumbControl() { Add(breadcrumbs = new BreadcrumbControl { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index d8a4514df1..35e5f9719c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -67,9 +67,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddRepeatStep(@"add many simple", sendManyNotifications, 3); - AddWaitStep("wait some", 5); - - checkProgressingCount(0); + waitForCompletion(); AddStep(@"progress #3", sendUploadProgress); @@ -77,9 +75,7 @@ namespace osu.Game.Tests.Visual.UserInterface checkDisplayedCount(33); - AddWaitStep("wait some", 10); - - checkProgressingCount(0); + waitForCompletion(); } [Test] @@ -109,9 +105,9 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep(@"background progress #1", sendBackgroundUploadProgress); - AddWaitStep("wait some", 5); + checkProgressingCount(1); - checkProgressingCount(0); + waitForCompletion(); checkDisplayedCount(2); @@ -190,6 +186,8 @@ namespace osu.Game.Tests.Visual.UserInterface private void checkProgressingCount(int expected) => AddAssert($"progressing count is {expected}", () => progressingNotifications.Count == expected); + private void waitForCompletion() => AddUntilStep("wait for notification progress completion", () => progressingNotifications.Count == 0); + private void sendBarrage() { switch (RNG.Next(0, 4)) diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 6c799e5e90..3f8a548fd5 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 7ecfd6ef70..75b88be1ab 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 4d7abfe272..bd91ad9704 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tournament [BackgroundDependencyLoader] private void load(Storage storage, FrameworkConfigManager frameworkConfig) { - Resources.AddStore(new DllResourceStore(@"osu.Game.Tournament.dll")); + Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); AddFont(Resources, @"Resources/Fonts/Aquatico-Regular"); AddFont(Resources, @"Resources/Fonts/Aquatico-Light"); diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 23a74d3fa6..f6b0107bd2 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -17,11 +17,6 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; - /// - /// An optional ruleset namespace. - /// - public string Namespace; - /// /// The bank to load the sample from. /// @@ -49,15 +44,6 @@ namespace osu.Game.Audio { get { - if (!string.IsNullOrEmpty(Namespace)) - { - if (!string.IsNullOrEmpty(Suffix)) - yield return $"{Namespace}/{Bank}-{Name}{Suffix}"; - - yield return $"{Namespace}/{Bank}-{Name}"; - } - - // check non-namespace as a fallback even when we have a namespace if (!string.IsNullOrEmpty(Suffix)) yield return $"{Bank}-{Name}{Suffix}"; diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 7922843626..99e0bf4e33 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; namespace osu.Game.Beatmaps @@ -25,7 +26,7 @@ namespace osu.Game.Beatmaps public IBeatmap Beatmap { get; } - protected BeatmapConverter(IBeatmap beatmap) + protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; } @@ -33,7 +34,7 @@ namespace osu.Game.Beatmaps /// /// Whether can be converted by this . /// - public bool CanConvert => !Beatmap.HitObjects.Any() || ValidConversionTypes.All(t => Beatmap.HitObjects.Any(t.IsInstanceOfType)); + public abstract bool CanConvert(); /// /// Converts . @@ -92,11 +93,6 @@ namespace osu.Game.Beatmaps return result; } - /// - /// The types of HitObjects that can be converted to be used for this Beatmap. - /// - protected abstract IEnumerable ValidConversionTypes { get; } - /// /// Creates the that will be returned by this . /// diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 46efe38d37..bfcc38e4a9 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -76,7 +76,7 @@ namespace osu.Game.Beatmaps public IBeatmap Beatmap { get; set; } - public bool CanConvert => true; + public bool CanConvert() => true; public IBeatmap Convert() { diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index f2213b85f1..173d5494ba 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps /// /// Whether can be converted by this . /// - bool CanConvert { get; } + bool CanConvert(); /// /// Converts . diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 1255665cf0..6aba5257f5 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -108,7 +108,7 @@ namespace osu.Game.Beatmaps IBeatmapConverter converter = CreateBeatmapConverter(Beatmap, rulesetInstance); // Check if the beatmap can be converted - if (!converter.CanConvert) + if (Beatmap.HitObjects.Count > 0 && !converter.CanConvert()) throw new BeatmapInvalidForRulesetException($"{nameof(Beatmaps.Beatmap)} can not be converted for the ruleset (ruleset: {ruleset.InstantiationInfo}, converter: {converter})."); // Apply conversion mods diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fd455d7cd5..45fe034a70 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -259,6 +259,9 @@ namespace osu.Game.Database /// /// Create a SHA-2 hash from the provided archive based on file content of all files matching . /// + /// + /// In the case of no matching files, a hash will be generated from the passed archive's . + /// private string computeHash(ArchiveReader reader) { // for now, concatenate all .osu files in the set to create a unique hash. @@ -270,7 +273,7 @@ namespace osu.Game.Database s.CopyTo(hashable); } - return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : null; + return hashable.Length > 0 ? hashable.ComputeSHA2Hash() : reader.Name.ComputeSHA2Hash(); } /// diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 6d88808150..af492bacc9 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -29,8 +29,33 @@ namespace osu.Game.Graphics.Backgrounds /// private const float edge_smoothness = 1; - public Color4 ColourLight = Color4.White; - public Color4 ColourDark = Color4.Black; + private Color4 colourLight = Color4.White; + + public Color4 ColourLight + { + get => colourLight; + set + { + if (colourLight == value) return; + + colourLight = value; + updateColours(); + } + } + + private Color4 colourDark = Color4.Black; + + public Color4 ColourDark + { + get => colourDark; + set + { + if (colourDark == value) return; + + colourDark = value; + updateColours(); + } + } /// /// Whether we want to expire triangles as they exit our draw area completely. @@ -151,7 +176,8 @@ namespace osu.Game.Graphics.Backgrounds TriangleParticle particle = CreateTriangle(); particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); - particle.Colour = CreateTriangleShade(); + particle.ColourShade = RNG.NextSingle(); + particle.Colour = CreateTriangleShade(particle.ColourShade); return particle; } @@ -177,7 +203,17 @@ namespace osu.Game.Graphics.Backgrounds /// Creates a shade of colour for the triangles. /// /// The colour. - protected virtual Color4 CreateTriangleShade() => Interpolation.ValueAt(RNG.NextSingle(), ColourDark, ColourLight, 0, 1); + protected virtual Color4 CreateTriangleShade(float shade) => Interpolation.ValueAt(shade, colourDark, colourLight, 0, 1); + + private void updateColours() + { + for (int i = 0; i < parts.Count; i++) + { + TriangleParticle newParticle = parts[i]; + newParticle.Colour = CreateTriangleShade(newParticle.ColourShade); + parts[i] = newParticle; + } + } protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this); @@ -264,6 +300,12 @@ namespace osu.Game.Graphics.Backgrounds /// public Vector2 Position; + /// + /// The colour shade of the triangle. + /// This is needed for colour recalculation of visible triangles when or is changed. + /// + public float ColourShade; + /// /// The colour of the triangle. /// diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index d1e55fee24..e2438cc4cd 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -15,14 +15,13 @@ namespace osu.Game.Graphics.UserInterface public class BreadcrumbControl : OsuTabControl { private const float padding = 10; - private const float item_chevron_size = 10; protected override TabItem CreateTabItem(T value) => new BreadcrumbTabItem(value) { AccentColour = AccentColour, }; - protected override float StripWidth() => base.StripWidth() - (padding + item_chevron_size); + protected override float StripWidth => base.StripWidth - TabContainer.FirstOrDefault()?.Padding.Right ?? 0; public BreadcrumbControl() { @@ -41,8 +40,10 @@ namespace osu.Game.Graphics.UserInterface }; } - private class BreadcrumbTabItem : OsuTabItem, IStateful + protected class BreadcrumbTabItem : OsuTabItem, IStateful { + protected virtual float ChevronSize => 10; + public event Action StateChanged; public readonly SpriteIcon Chevron; @@ -52,7 +53,6 @@ namespace osu.Game.Graphics.UserInterface public override bool HandleNonPositionalInput => State == Visibility.Visible; public override bool HandlePositionalInput => State == Visibility.Visible; - public override bool IsRemovable => true; private Visibility state; @@ -91,12 +91,12 @@ namespace osu.Game.Graphics.UserInterface { Text.Font = Text.Font.With(size: 18); Text.Margin = new MarginPadding { Vertical = 8 }; - Padding = new MarginPadding { Right = padding + item_chevron_size }; + Padding = new MarginPadding { Right = padding + ChevronSize }; Add(Chevron = new SpriteIcon { Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, - Size = new Vector2(item_chevron_size), + Size = new Vector2(ChevronSize), Icon = FontAwesome.Solid.ChevronRight, Margin = new MarginPadding { Left = padding }, Alpha = 0f, diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index 563dc2dad9..958390d5d2 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; @@ -31,6 +32,7 @@ namespace osu.Game.Graphics.UserInterface protected readonly Nub Nub; private readonly Box leftBox; private readonly Box rightBox; + private readonly Container nubContainer; public virtual string TooltipText { get; private set; } @@ -72,10 +74,15 @@ namespace osu.Game.Graphics.UserInterface Origin = Anchor.CentreRight, Alpha = 0.5f, }, - Nub = new Nub + nubContainer = new Container { - Origin = Anchor.TopCentre, - Expanded = true, + RelativeSizeAxes = Axes.Both, + Child = Nub = new Nub + { + Origin = Anchor.TopCentre, + RelativePositionAxes = Axes.X, + Expanded = true, + }, }, new HoverClickSounds() }; @@ -90,6 +97,13 @@ namespace osu.Game.Graphics.UserInterface AccentColour = colours.Pink; } + protected override void Update() + { + base.Update(); + + nubContainer.Padding = new MarginPadding { Horizontal = RangePadding }; + } + protected override void LoadComplete() { base.LoadComplete(); @@ -176,14 +190,14 @@ namespace osu.Game.Graphics.UserInterface { base.UpdateAfterChildren(); leftBox.Scale = new Vector2(Math.Clamp( - Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); + RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); rightBox.Scale = new Vector2(Math.Clamp( - DrawWidth - Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, DrawWidth), 1); + DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, DrawWidth), 1); } protected override void UpdateValue(float value) { - Nub.MoveToX(RangePadding + UsableWidth * value, 250, Easing.OutQuint); + Nub.MoveToX(value, 250, Easing.OutQuint); } /// diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 064cba6adf..ed8904db7e 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -28,8 +28,7 @@ namespace osu.Game.Graphics.UserInterface protected override TabItem CreateTabItem(T value) => new OsuTabItem(value); - protected virtual float StripWidth() => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; - protected virtual float StripHeight() => 1; + protected virtual float StripWidth => TabContainer.Children.Sum(c => c.IsPresent ? c.DrawWidth + TabContainer.Spacing.X : 0) - TabContainer.Spacing.X; /// /// Whether entries should be automatically populated if is an type. @@ -46,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Height = StripHeight(), + Height = 1, Colour = Color4.White.Opacity(0), }); @@ -99,7 +98,7 @@ namespace osu.Game.Graphics.UserInterface // dont bother calculating if the strip is invisible if (strip.Colour.MaxAlpha > 0) - strip.Width = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 1000), strip.Width, StripWidth(), 0, 500, Easing.OutQuint); + strip.Width = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 1000), strip.Width, StripWidth, 0, 500, Easing.OutQuint); } public class OsuTabItem : TabItem, IHasAccentColour diff --git a/osu.Game/Graphics/UserInterface/ScreenTitle.cs b/osu.Game/Graphics/UserInterface/ScreenTitle.cs index 10fc312d8b..ecd0508258 100644 --- a/osu.Game/Graphics/UserInterface/ScreenTitle.cs +++ b/osu.Game/Graphics/UserInterface/ScreenTitle.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using osuTK; @@ -13,15 +14,15 @@ namespace osu.Game.Graphics.UserInterface { public abstract class ScreenTitle : CompositeDrawable, IHasAccentColour { - public const float ICON_WIDTH = ICON_SIZE + icon_spacing; + public const float ICON_WIDTH = ICON_SIZE + spacing; public const float ICON_SIZE = 25; + private const float spacing = 6; + private const int text_offset = 2; private SpriteIcon iconSprite; private readonly OsuSpriteText titleText, pageText; - private const float icon_spacing = 10; - protected IconUsage Icon { set @@ -63,26 +64,35 @@ namespace osu.Game.Graphics.UserInterface new FillFlowContainer { AutoSizeAxes = Axes.Both, - Spacing = new Vector2(icon_spacing, 0), + Spacing = new Vector2(spacing, 0), + Direction = FillDirection.Horizontal, Children = new[] { - CreateIcon(), - new FillFlowContainer + CreateIcon().With(t => { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(6, 0), - Children = new[] - { - titleText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), - }, - pageText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light), - } - } + t.Anchor = Anchor.Centre; + t.Origin = Anchor.Centre; + }), + titleText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), + Margin = new MarginPadding { Bottom = text_offset } + }, + new Circle + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(4), + Colour = Color4.Gray, + }, + pageText = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20), + Margin = new MarginPadding { Bottom = text_offset } } } }, diff --git a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs index f590e7e357..c2a13970de 100644 --- a/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs +++ b/osu.Game/Graphics/UserInterface/ScreenTitleTextureIcon.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osuTK; @@ -16,8 +15,6 @@ namespace osu.Game.Graphics.UserInterface /// public class ScreenTitleTextureIcon : CompositeDrawable { - private const float circle_allowance = 0.8f; - private readonly string textureName; public ScreenTitleTextureIcon(string textureName) @@ -26,38 +23,17 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(TextureStore textures, OsuColour colours) + private void load(TextureStore textures) { - Size = new Vector2(ScreenTitle.ICON_SIZE / circle_allowance); + Size = new Vector2(ScreenTitle.ICON_SIZE); - InternalChildren = new Drawable[] + InternalChild = new Sprite { - new CircularContainer - { - Masking = true, - BorderColour = colours.Violet, - BorderThickness = 3, - MaskingSmoothness = 1, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = textures.Get(textureName), - Size = new Vector2(circle_allowance), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colours.Violet, - Alpha = 0, - AlwaysPresent = true, - }, - } - }, + RelativeSizeAxes = Axes.Both, + Texture = textures.Get(textureName), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fit }; } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index ff3618b263..fe8756a4d2 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -34,11 +34,21 @@ namespace osu.Game.Graphics.UserInterface public override bool OnPressed(PlatformAction action) { - // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox - // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text) - // Avoid handling it here to allow other components to potentially consume the shortcut. - if (action.ActionType == PlatformActionType.CharNext && action.ActionMethod == PlatformActionMethod.Delete) - return false; + switch (action.ActionType) + { + case PlatformActionType.LineEnd: + case PlatformActionType.LineStart: + return false; + + // Shift+delete is handled via PlatformAction on macOS. this is not so useful in the context of a SearchTextBox + // as we do not allow arrow key navigation in the first place (ie. the caret should always be at the end of text) + // Avoid handling it here to allow other components to potentially consume the shortcut. + case PlatformActionType.CharNext: + if (action.ActionMethod == PlatformActionMethod.Delete) + return false; + + break; + } return base.OnPressed(action); } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index bf758e21d9..e37567c72c 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -32,6 +32,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), + new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c7c746bed3..84aba4af52 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -39,6 +39,7 @@ using osu.Game.Online.Chat; using osu.Game.Skinning; using osuTK.Graphics; using osu.Game.Overlays.Volume; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select; using osu.Game.Utils; @@ -204,6 +205,7 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); + SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } @@ -403,9 +405,29 @@ namespace osu.Game oldBeatmap.Track.Completed -= currentTrackCompleted; } + updateModDefaults(); + nextBeatmap?.LoadBeatmapAsync(); } + private void modsChanged(ValueChangedEvent> mods) + { + updateModDefaults(); + } + + private void updateModDefaults() + { + BeatmapDifficulty baseDifficulty = Beatmap.Value.BeatmapInfo.BaseDifficulty; + + if (baseDifficulty != null && SelectedMods.Value.Any(m => m is IApplicableToDifficulty)) + { + var adjustedDifficulty = baseDifficulty.Clone(); + + foreach (var mod in SelectedMods.Value.OfType()) + mod.ReadFromDifficulty(adjustedDifficulty); + } + } + private void currentTrackCompleted() => Schedule(() => { if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 22b8d9d012..0819642d2d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -29,6 +29,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -125,7 +126,7 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - Resources.AddStore(new DllResourceStore(@"osu.Game.Resources.dll")); + Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 6de14c51ee..e530ff5489 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -196,7 +196,7 @@ namespace osu.Game.Overlays.AccountCreation return; } - api.Login(emailTextBox.Text, passwordTextBox.Text); + api.Login(usernameTextBox.Text, passwordTextBox.Text); }); }); } diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs new file mode 100644 index 0000000000..8a82b1f0c0 --- /dev/null +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs @@ -0,0 +1,39 @@ +// 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.UserInterface; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays +{ + public abstract class BreadcrumbControlOverlayHeader : OverlayHeader + { + protected OverlayHeaderBreadcrumbControl BreadcrumbControl; + + protected override TabControl CreateTabControl() => BreadcrumbControl = new OverlayHeaderBreadcrumbControl(); + + public class OverlayHeaderBreadcrumbControl : BreadcrumbControl + { + public OverlayHeaderBreadcrumbControl() + { + RelativeSizeAxes = Axes.X; + } + + protected override TabItem CreateTabItem(string value) => new ControlTabItem(value); + + private class ControlTabItem : BreadcrumbTabItem + { + protected override float ChevronSize => 8; + + public ControlTabItem(string value) + : base(value) + { + Text.Font = Text.Font.With(size: 14); + Chevron.Y = 3; + Bar.Height = 0; + } + } + } + } +} diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index b2e9be24b3..7e47a3e29f 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -15,7 +15,7 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Changelog { - public class ChangelogHeader : OverlayHeader + public class ChangelogHeader : BreadcrumbControlOverlayHeader { public readonly Bindable Current = new Bindable(); @@ -23,12 +23,12 @@ namespace osu.Game.Overlays.Changelog public UpdateStreamBadgeArea Streams; - private const string listing_string = "Listing"; + private const string listing_string = "listing"; public ChangelogHeader() { - TabControl.AddItem(listing_string); - TabControl.Current.ValueChanged += e => + BreadcrumbControl.AddItem(listing_string); + BreadcrumbControl.Current.ValueChanged += e => { if (e.NewValue == listing_string) ListingSelected?.Invoke(); @@ -46,7 +46,9 @@ namespace osu.Game.Overlays.Changelog [BackgroundDependencyLoader] private void load(OsuColour colours) { - TabControl.AccentColour = colours.Violet; + BreadcrumbControl.AccentColour = colours.Violet; + TitleBackgroundColour = colours.GreyVioletDarker; + ControlBackgroundColour = colours.GreyVioletDark; } private ChangelogHeaderTitle title; @@ -54,12 +56,12 @@ namespace osu.Game.Overlays.Changelog private void showBuild(ValueChangedEvent e) { if (e.OldValue != null) - TabControl.RemoveItem(e.OldValue.ToString()); + BreadcrumbControl.RemoveItem(e.OldValue.ToString()); if (e.NewValue != null) { - TabControl.AddItem(e.NewValue.ToString()); - TabControl.Current.Value = e.NewValue.ToString(); + BreadcrumbControl.AddItem(e.NewValue.ToString()); + BreadcrumbControl.Current.Value = e.NewValue.ToString(); Streams.Current.Value = Streams.Items.FirstOrDefault(s => s.Name == e.NewValue.UpdateStream.Name); @@ -67,7 +69,7 @@ namespace osu.Game.Overlays.Changelog } else { - TabControl.Current.Value = listing_string; + BreadcrumbControl.Current.Value = listing_string; Streams.Current.Value = null; title.Version = null; } @@ -111,7 +113,7 @@ namespace osu.Game.Overlays.Changelog public ChangelogHeaderTitle() { - Title = "Changelog"; + Title = "changelog"; Version = null; } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index fbc9dfcbd9..15b0079277 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -158,7 +158,8 @@ namespace osu.Game.Overlays private Task initialFetchTask; - private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ => Schedule(action)); + private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ => + Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); private Task fetchListing() { @@ -185,10 +186,10 @@ namespace osu.Game.Overlays tcs.SetResult(true); }); - req.Failure += _ => + req.Failure += e => { initialFetchTask = null; - tcs.SetResult(false); + tcs.SetException(e); }; await API.PerformAsync(req); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 33bcc4c139..e80c51d82a 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -171,6 +171,7 @@ namespace osu.Game.Overlays d.Origin = Anchor.BottomLeft; d.RelativeSizeAxes = Axes.Both; d.OnRequestLeave = channelManager.LeaveChannel; + d.IsSwitchable = true; }), } }, diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 7ae6efda6a..bdae9da226 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using System.Linq; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Comments { @@ -28,10 +29,16 @@ namespace osu.Game.Overlays.Comments private readonly BindableBool childrenExpanded = new BindableBool(true); - private readonly FillFlowContainer childCommentsVisibilityContainer; + private FillFlowContainer childCommentsVisibilityContainer; private readonly Comment comment; public DrawableComment(Comment comment) + { + this.comment = comment; + } + + [BackgroundDependencyLoader] + private void load() { LinkFlowContainer username; FillFlowContainer childCommentsContainer; @@ -41,8 +48,6 @@ namespace osu.Game.Overlays.Comments GridContainer content; VotePill votePill; - this.comment = comment; - RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; InternalChild = new FillFlowContainer diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index c1c5113c5e..4ad8e95512 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.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; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -9,21 +10,25 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; namespace osu.Game.Overlays.Direct { - public abstract class DirectPanel : Container + public abstract class DirectPanel : OsuClickableContainer, IHasContextMenu { public readonly BeatmapSetInfo SetInfo; @@ -32,8 +37,6 @@ namespace osu.Game.Overlays.Direct private Container content; - private BeatmapSetOverlay beatmapSetOverlay; - public PreviewTrack Preview => PlayButton.Preview; public Bindable PreviewPlaying => PlayButton?.Playing; @@ -44,6 +47,8 @@ namespace osu.Game.Overlays.Direct protected override Container Content => content; + protected Action ViewBeatmap; + protected DirectPanel(BeatmapSetInfo setInfo) { Debug.Assert(setInfo.OnlineBeatmapSetID != null); @@ -70,8 +75,6 @@ namespace osu.Game.Overlays.Direct [BackgroundDependencyLoader(permitNulls: true)] private void load(BeatmapManager beatmaps, OsuColour colours, BeatmapSetOverlay beatmapSetOverlay) { - this.beatmapSetOverlay = beatmapSetOverlay; - AddInternal(content = new Container { RelativeSizeAxes = Axes.Both, @@ -88,6 +91,12 @@ namespace osu.Game.Overlays.Direct }, } }); + + Action = ViewBeatmap = () => + { + Debug.Assert(SetInfo.OnlineBeatmapSetID != null); + beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); + }; } protected override void Update() @@ -120,13 +129,6 @@ namespace osu.Game.Overlays.Direct base.OnHoverLost(e); } - protected override bool OnClick(ClickEvent e) - { - Debug.Assert(SetInfo.OnlineBeatmapSetID != null); - beatmapSetOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); - return true; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -203,5 +205,10 @@ namespace osu.Game.Overlays.Direct Value = value; } } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("View Beatmap", MenuItemType.Highlighted, ViewBeatmap), + }; } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 27620ab523..fc88c86df2 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Graphics; @@ -13,9 +12,9 @@ using System; namespace osu.Game.Overlays.News { - public class NewsHeader : OverlayHeader + public class NewsHeader : BreadcrumbControlOverlayHeader { - private const string front_page_string = "Front Page"; + private const string front_page_string = "frontpage"; private NewsHeaderTitle title; @@ -25,46 +24,46 @@ namespace osu.Game.Overlays.News public NewsHeader() { - TabControl.AddItem(front_page_string); + BreadcrumbControl.AddItem(front_page_string); - TabControl.Current.ValueChanged += e => + BreadcrumbControl.Current.ValueChanged += e => { if (e.NewValue == front_page_string) ShowFrontPage?.Invoke(); }; - Current.ValueChanged += showArticle; + Current.ValueChanged += showPost; } [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load(OsuColour colours) { - TabControl.AccentColour = colour.Violet; + BreadcrumbControl.AccentColour = colours.Violet; + TitleBackgroundColour = colours.GreyVioletDarker; + ControlBackgroundColour = colours.GreyVioletDark; } - private void showArticle(ValueChangedEvent e) + private void showPost(ValueChangedEvent e) { if (e.OldValue != null) - TabControl.RemoveItem(e.OldValue); + BreadcrumbControl.RemoveItem(e.OldValue); if (e.NewValue != null) { - TabControl.AddItem(e.NewValue); - TabControl.Current.Value = e.NewValue; + BreadcrumbControl.AddItem(e.NewValue); + BreadcrumbControl.Current.Value = e.NewValue; - title.IsReadingArticle = true; + title.IsReadingPost = true; } else { - TabControl.Current.Value = front_page_string; - title.IsReadingArticle = false; + BreadcrumbControl.Current.Value = front_page_string; + title.IsReadingPost = false; } } protected override Drawable CreateBackground() => new NewsHeaderBackground(); - protected override Drawable CreateContent() => new Container(); - protected override ScreenTitle CreateTitle() => title = new NewsHeaderTitle(); private class NewsHeaderBackground : Sprite @@ -84,17 +83,17 @@ namespace osu.Game.Overlays.News private class NewsHeaderTitle : ScreenTitle { - private const string article_string = "Article"; + private const string post_string = "post"; - public bool IsReadingArticle + public bool IsReadingPost { - set => Section = value ? article_string : front_page_string; + set => Section = value ? post_string : front_page_string; } public NewsHeaderTitle() { - Title = "News"; - IsReadingArticle = false; + Title = "news"; + IsReadingPost = false; } protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/news"); diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index 2e032db2ba..53da2da634 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -1,70 +1,104 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; +using osuTK.Graphics; namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { - protected readonly OverlayHeaderTabControl TabControl; + private readonly Box titleBackground; + private readonly Box controlBackground; + private readonly Container background; - private const float cover_height = 150; - private const float cover_info_height = 75; + protected Color4 TitleBackgroundColour + { + set => titleBackground.Colour = value; + } + + protected Color4 ControlBackgroundColour + { + set => controlBackground.Colour = value; + } + + protected float BackgroundHeight + { + set => background.Height = value; + } protected OverlayHeader() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Children = new Drawable[] + Add(new FillFlowContainer { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new[] { - RelativeSizeAxes = Axes.X, - Height = cover_height, - Masking = true, - Child = CreateBackground() - }, - new Container - { - Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }, - Y = cover_height, - Height = cover_info_height, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.TopLeft, - Origin = Anchor.BottomLeft, - Depth = -float.MaxValue, - Children = new Drawable[] + background = new Container { - CreateTitle().With(t => t.X = -ScreenTitle.ICON_WIDTH), - TabControl = new OverlayHeaderTabControl + RelativeSizeAxes = Axes.X, + Height = 80, + Masking = true, + Child = CreateBackground() + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = cover_info_height - 30, - Margin = new MarginPadding { Left = -UserProfileOverlay.CONTENT_X_MARGIN }, - Padding = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN } + titleBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + CreateTitle().With(title => + { + title.Margin = new MarginPadding + { + Vertical = 10, + Left = UserProfileOverlay.CONTENT_X_MARGIN + }; + }) } - } - }, - new Container - { - Margin = new MarginPadding { Top = cover_height }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = CreateContent() + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Depth = -float.MaxValue, + Children = new Drawable[] + { + controlBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Gray, + }, + CreateTabControl().With(control => control.Margin = new MarginPadding { Left = UserProfileOverlay.CONTENT_X_MARGIN }) + } + }, + CreateContent() } - }; + }); } protected abstract Drawable CreateBackground(); - protected abstract Drawable CreateContent(); + [NotNull] + protected virtual Drawable CreateContent() => new Container(); protected abstract ScreenTitle CreateTitle(); + + protected abstract TabControl CreateTabControl(); } } diff --git a/osu.Game/Overlays/OverlayHeaderTabControl.cs b/osu.Game/Overlays/OverlayHeaderTabControl.cs deleted file mode 100644 index 7d0cdad6d8..0000000000 --- a/osu.Game/Overlays/OverlayHeaderTabControl.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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.UserInterface; - -namespace osu.Game.Overlays -{ - public class OverlayHeaderTabControl : OverlayTabControl - { - protected override TabItem CreateTabItem(string value) => new OverlayHeaderTabItem(value) - { - AccentColour = AccentColour, - }; - - private class OverlayHeaderTabItem : OverlayTabItem - { - public OverlayHeaderTabItem(string value) - : base(value) - { - Text.Text = value; - } - } - } -} diff --git a/osu.Game/Overlays/OverlayTabControl.cs b/osu.Game/Overlays/OverlayTabControl.cs index 4c396eabc1..812f8963c9 100644 --- a/osu.Game/Overlays/OverlayTabControl.cs +++ b/osu.Game/Overlays/OverlayTabControl.cs @@ -43,6 +43,11 @@ namespace osu.Game.Overlays set => TabContainer.Padding = value; } + protected float BarHeight + { + set => bar.Height = value; + } + protected OverlayTabControl() { TabContainer.Masking = false; @@ -63,8 +68,7 @@ namespace osu.Game.Overlays protected class OverlayTabItem : TabItem { - private readonly ExpandingBar bar; - + protected readonly ExpandingBar Bar; protected readonly OsuSpriteText Text; private Color4 accentColour; @@ -78,7 +82,7 @@ namespace osu.Game.Overlays return; accentColour = value; - bar.Colour = value; + Bar.Colour = value; updateState(); } @@ -99,7 +103,7 @@ namespace osu.Game.Overlays Anchor = Anchor.BottomLeft, Font = OsuFont.GetFont(), }, - bar = new ExpandingBar + Bar = new ExpandingBar { Anchor = Anchor.BottomCentre, ExpandedSize = 7.5f, @@ -149,13 +153,13 @@ namespace osu.Game.Overlays protected virtual void HoverAction() { - bar.Expand(); + Bar.Expand(); Text.FadeColour(Color4.White, 120, Easing.InQuad); } protected virtual void UnhoverAction() { - bar.Collapse(); + Bar.Collapse(); Text.FadeColour(AccentColour, 120, Easing.InQuad); } } diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 76613c156d..59e64dfc26 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -15,7 +15,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile { - public class ProfileHeader : OverlayHeader + public class ProfileHeader : TabControlOverlayHeader { private UserCoverBackground coverContainer; @@ -26,10 +26,12 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { + BackgroundHeight = 150; + User.ValueChanged += e => updateDisplay(e.NewValue); - TabControl.AddItem("Info"); - TabControl.AddItem("Modding"); + TabControl.AddItem("info"); + TabControl.AddItem("modding"); centreHeaderContainer.DetailsVisible.BindValueChanged(visible => detailHeaderContainer.Expanded = visible.NewValue, true); } @@ -38,6 +40,8 @@ namespace osu.Game.Overlays.Profile private void load(OsuColour colours) { TabControl.AccentColour = colours.Seafoam; + TitleBackgroundColour = colours.GreySeafoamDarker; + ControlBackgroundColour = colours.GreySeafoam; } protected override Drawable CreateBackground() => @@ -101,8 +105,8 @@ namespace osu.Game.Overlays.Profile { public ProfileHeaderTitle() { - Title = "Player"; - Section = "Info"; + Title = "player"; + Section = "info"; } [BackgroundDependencyLoader] @@ -110,6 +114,8 @@ namespace osu.Game.Overlays.Profile { AccentColour = colours.Seafoam; } + + protected override Drawable CreateIcon() => new ScreenTitleTextureIcon(@"Icons/profile"); } } } diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 37478d902b..5975e94ffc 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; namespace osu.Game.Overlays.SearchableList { @@ -61,21 +62,20 @@ namespace osu.Game.Overlays.SearchableList scrollContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new[] + Child = new OsuContextMenuContainer { - new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, - Children = new[] + Child = ScrollFlow = new FillFlowContainer { - ScrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, - Direction = FillDirection.Vertical, - }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = WIDTH_PADDING, Bottom = 50 }, + Direction = FillDirection.Vertical, }, }, }, diff --git a/osu.Game/Overlays/Settings/ISettingsItem.cs b/osu.Game/Overlays/Settings/ISettingsItem.cs new file mode 100644 index 0000000000..e7afa48502 --- /dev/null +++ b/osu.Game/Overlays/Settings/ISettingsItem.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. + +using System; +using osu.Framework.Graphics; + +namespace osu.Game.Overlays.Settings +{ + public interface ISettingsItem : IDrawable, IDisposable + { + event Action SettingChanged; + } +} diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 31fcb7abd8..35f28ab1b2 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.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; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -20,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable + public abstract class SettingsItem : Container, IFilterable, ISettingsItem { protected abstract Drawable CreateControl(); @@ -34,8 +35,6 @@ namespace osu.Game.Overlays.Settings private SpriteText text; - private readonly RestoreDefaultValueButton restoreDefaultButton; - public bool ShowsDefaultIndicator = true; public virtual string LabelText @@ -70,8 +69,12 @@ namespace osu.Game.Overlays.Settings public bool FilteringActive { get; set; } + public event Action SettingChanged; + protected SettingsItem() { + RestoreDefaultValueButton restoreDefaultButton; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS }; @@ -87,13 +90,12 @@ namespace osu.Game.Overlays.Settings Child = Control = CreateControl() }, }; - } - [BackgroundDependencyLoader] - private void load() - { + // all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is + // never loaded, but requires bindable storage. if (controlWithCurrent != null) { + controlWithCurrent.Current.ValueChanged += _ => SettingChanged?.Invoke(); controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; if (ShowsDefaultIndicator) diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs new file mode 100644 index 0000000000..f3521b66c8 --- /dev/null +++ b/osu.Game/Overlays/TabControlOverlayHeader.cs @@ -0,0 +1,55 @@ +// 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.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Overlays +{ + public abstract class TabControlOverlayHeader : OverlayHeader + { + protected OverlayHeaderTabControl TabControl; + + protected override TabControl CreateTabControl() => TabControl = new OverlayHeaderTabControl(); + + public class OverlayHeaderTabControl : OverlayTabControl + { + public OverlayHeaderTabControl() + { + BarHeight = 1; + RelativeSizeAxes = Axes.None; + AutoSizeAxes = Axes.X; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + Height = 35; + } + + protected override TabItem CreateTabItem(string value) => new OverlayHeaderTabItem(value) + { + AccentColour = AccentColour, + }; + + protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + }; + + private class OverlayHeaderTabItem : OverlayTabItem + { + public OverlayHeaderTabItem(string value) + : base(value) + { + Text.Text = value; + Text.Font = OsuFont.GetFont(size: 14); + Bar.ExpandedSize = 5; + } + } + } + } +} diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 4710465536..89e7866707 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Edit private readonly DrawableRuleset drawableRuleset; [Resolved] - private IEditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } public DrawableEditRulesetWrapper(DrawableRuleset drawableRuleset) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 22d94abcb9..bfaa7e8872 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -35,20 +34,20 @@ namespace osu.Game.Rulesets.Edit { protected IRulesetConfigManager Config { get; private set; } - protected new EditorBeatmap EditorBeatmap { get; private set; } - protected readonly Ruleset Ruleset; [Resolved] protected IFrameBasedClock EditorClock { get; private set; } + [Resolved] + protected EditorBeatmap EditorBeatmap { get; private set; } + [Resolved] private IAdjustableClock adjustableClock { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - private Beatmap playableBeatmap; private IBeatmapProcessor beatmapProcessor; private DrawableEditRulesetWrapper drawableRulesetWrapper; @@ -68,9 +67,17 @@ namespace osu.Game.Rulesets.Edit [BackgroundDependencyLoader] private void load(IFrameBasedClock framedClock) { + beatmapProcessor = Ruleset.CreateBeatmapProcessor(EditorBeatmap.PlayableBeatmap); + + EditorBeatmap.HitObjectAdded += addHitObject; + EditorBeatmap.HitObjectRemoved += removeHitObject; + EditorBeatmap.StartTimeChanged += UpdateHitObject; + + Config = Dependencies.Get().GetConfigFor(Ruleset); + try { - drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, playableBeatmap)) + drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) { Clock = framedClock, ProcessCustomClock = false @@ -140,28 +147,6 @@ namespace osu.Game.Rulesets.Edit blueprintContainer.SelectionChanged += selectionChanged; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var parentWorkingBeatmap = parent.Get>().Value; - - playableBeatmap = (Beatmap)parentWorkingBeatmap.GetPlayableBeatmap(Ruleset.RulesetInfo); - - beatmapProcessor = Ruleset.CreateBeatmapProcessor(playableBeatmap); - - base.EditorBeatmap = EditorBeatmap = new EditorBeatmap(playableBeatmap); - EditorBeatmap.HitObjectAdded += addHitObject; - EditorBeatmap.HitObjectRemoved += removeHitObject; - EditorBeatmap.StartTimeChanged += UpdateHitObject; - - var dependencies = new DependencyContainer(parent); - dependencies.CacheAs(EditorBeatmap); - dependencies.CacheAs>(EditorBeatmap); - - Config = dependencies.Get().GetConfigFor(Ruleset); - - return base.CreateChildDependencies(dependencies); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -234,7 +219,7 @@ namespace osu.Game.Rulesets.Edit scheduledUpdate = Schedule(() => { beatmapProcessor?.PreProcess(); - hitObject?.ApplyDefaults(playableBeatmap.ControlPointInfo, playableBeatmap.BeatmapInfo.BaseDifficulty); + hitObject?.ApplyDefaults(EditorBeatmap.ControlPointInfo, EditorBeatmap.BeatmapInfo.BaseDifficulty); beatmapProcessor?.PostProcess(); }); } @@ -333,11 +318,6 @@ namespace osu.Game.Rulesets.Edit /// public abstract IEnumerable HitObjects { get; } - /// - /// An editor-specific beatmap, exposing mutation events. - /// - public IEditorBeatmap EditorBeatmap { get; protected set; } - /// /// Whether the user's cursor is currently in an area of the that is valid for placement. /// diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs new file mode 100644 index 0000000000..06a85b5261 --- /dev/null +++ b/osu.Game/Rulesets/ILegacyRuleset.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.Rulesets +{ + public interface ILegacyRuleset + { + /// + /// Identifies the server-side ID of a legacy ruleset. + /// + int LegacyID { get; } + } +} diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index f07f76a2b8..599135ba54 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -11,6 +11,12 @@ namespace osu.Game.Rulesets.Judgements /// public class Judgement { + /// + /// The default health increase for a maximum judgement, as a proportion of total health. + /// By default, each maximum judgement restores 5% of total health. + /// + protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; + /// /// The maximum that can be achieved. /// @@ -55,7 +61,32 @@ namespace osu.Game.Rulesets.Judgements /// /// The to find the numeric health increase for. /// The numeric health increase of . - protected virtual double HealthIncreaseFor(HitResult result) => 0; + protected virtual double HealthIncreaseFor(HitResult result) + { + switch (result) + { + case HitResult.Miss: + return -DEFAULT_MAX_HEALTH_INCREASE; + + case HitResult.Meh: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + + case HitResult.Ok: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.01; + + case HitResult.Good: + return DEFAULT_MAX_HEALTH_INCREASE * 0.3; + + case HitResult.Great: + return DEFAULT_MAX_HEALTH_INCREASE; + + case HitResult.Perfect: + return DEFAULT_MAX_HEALTH_INCREASE * 1.05; + + default: + return 0; + } + } /// /// Retrieves the numeric health increase of a . diff --git a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs index 4d4cd75434..34198da722 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToDifficulty.cs @@ -10,6 +10,17 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToDifficulty : IApplicableMod { + /// + /// Called when a beatmap is changed. Can be used to read default values. + /// Any changes made will not be preserved. + /// + /// The difficulty to read from. + void ReadFromDifficulty(BeatmapDifficulty difficulty); + + /// + /// Called post beatmap conversion. Can be used to apply changes to difficulty attributes. + /// + /// The difficulty to mutate. void ApplyToDifficulty(BeatmapDifficulty difficulty); } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.cs new file mode 100644 index 0000000000..a181955653 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToHealthProcessor.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 osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public interface IApplicableToHealthProcessor : IApplicableMod + { + /// + /// Provide a to a mod. Called once on initialisation of a play instance. + /// + void ApplyToHealthProcessor(HealthProcessor healthProcessor); + } +} diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs new file mode 100644 index 0000000000..c5b8a1bc73 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -0,0 +1,105 @@ +// 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.Beatmaps; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using System; +using System.Collections.Generic; +using osu.Game.Configuration; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModDifficultyAdjust : Mod, IApplicableToDifficulty + { + public override string Name => @"Difficulty Adjust"; + + public override string Description => @"Override a beatmap's difficulty settings."; + + public override string Acronym => "DA"; + + public override ModType Type => ModType.Conversion; + + public override IconUsage Icon => FontAwesome.Solid.Hammer; + + public override double ScoreMultiplier => 1.0; + + public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModHardRock) }; + + [SettingSource("Drain Rate", "Override a beatmap's set HP.")] + public BindableNumber DrainRate { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + [SettingSource("Overall Difficulty", "Override a beatmap's set OD.")] + public BindableNumber OverallDifficulty { get; } = new BindableFloat + { + Precision = 0.1f, + MinValue = 1, + MaxValue = 10, + Default = 5, + Value = 5, + }; + + private BeatmapDifficulty difficulty; + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + if (this.difficulty == null || this.difficulty.ID != difficulty.ID) + { + TransferSettings(difficulty); + this.difficulty = difficulty; + } + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty); + + /// + /// Transfer initial settings from the beatmap to settings. + /// + /// The beatmap's initial values. + protected virtual void TransferSettings(BeatmapDifficulty difficulty) + { + TransferSetting(DrainRate, difficulty.DrainRate); + TransferSetting(OverallDifficulty, difficulty.OverallDifficulty); + } + + private readonly Dictionary userChangedSettings = new Dictionary(); + + /// + /// Transfer a setting from to a configuration bindable. + /// Only performs the transfer if the user it not currently overriding.. + /// + protected void TransferSetting(BindableNumber bindable, T beatmapDefault) + where T : struct, IComparable, IConvertible, IEquatable + { + bindable.UnbindEvents(); + + userChangedSettings.TryAdd(bindable, false); + + bindable.Default = beatmapDefault; + + // users generally choose a difficulty setting and want it to stick across multiple beatmap changes. + // we only want to value transfer if the user hasn't changed the value previously. + if (!userChangedSettings[bindable]) + bindable.Value = beatmapDefault; + + bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault; + } + + /// + /// Apply all custom settings to the provided beatmap. + /// + /// The beatmap to have settings applied. + protected virtual void ApplySettings(BeatmapDifficulty difficulty) + { + difficulty.DrainRate = DrainRate.Value; + difficulty.OverallDifficulty = OverallDifficulty.Value; + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index a55ebc51d6..ec0f50c0be 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -5,13 +5,13 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToScoreProcessor + public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor { public override string Name => "Easy"; public override string Acronym => "EZ"; @@ -19,12 +19,21 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyReduction; public override double ScoreMultiplier => 0.5; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; + public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - private int retries = 2; + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + + private int retries; private BindableNumber health; + public void ReadFromDifficulty(BeatmapDifficulty difficulty) { } + public void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; @@ -32,6 +41,8 @@ namespace osu.Game.Rulesets.Mods difficulty.ApproachRate *= ratio; difficulty.DrainRate *= ratio; difficulty.OverallDifficulty *= ratio; + + retries = Retries.Value; } public bool AllowFail @@ -49,11 +60,9 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { - health = scoreProcessor.Health.GetBoundCopy(); + health = healthProcessor.Health.GetBoundCopy(); } - - public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 2044cbeae2..a613d41cf4 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -15,7 +15,9 @@ namespace osu.Game.Rulesets.Mods public override IconUsage Icon => OsuIcon.ModHardrock; public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Everything just got a bit harder..."; - public override Type[] IncompatibleMods => new[] { typeof(ModEasy) }; + public override Type[] IncompatibleMods => new[] { typeof(ModEasy), typeof(ModDifficultyAdjust) }; + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) { } public void ApplyToDifficulty(BeatmapDifficulty difficulty) { diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 0994d1f7d3..afa263f1c9 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Mods public override IconUsage Icon => OsuIcon.ModPerfect; public override string Description => "SS or quit."; - protected override bool FailCondition(ScoreProcessor scoreProcessor, JudgementResult result) => scoreProcessor.Accuracy.Value != 1; + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type != result.Judgement.MaxResult; } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index c4c4ab1f04..a4d0631d8c 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -6,11 +6,10 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod, IApplicableToScoreProcessor, IApplicableFailOverride + public abstract class ModSuddenDeath : Mod, IApplicableToHealthProcessor, IApplicableFailOverride { public override string Name => "Sudden Death"; public override string Acronym => "SD"; @@ -24,13 +23,11 @@ namespace osu.Game.Rulesets.Mods public bool AllowFail => true; public bool RestartOnFail => true; - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { - scoreProcessor.FailConditions += FailCondition; + healthProcessor.FailConditions += FailCondition; } - public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; - - protected virtual bool FailCondition(ScoreProcessor scoreProcessor, JudgementResult result) => scoreProcessor.Combo.Value == 0 && result.Judgement.AffectsCombo; + protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !result.IsHit && result.Judgement.AffectsCombo; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a959fee9be..4ac30fe7fb 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -31,9 +31,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); - // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first - protected virtual string SampleNamespace => null; - protected SkinnableSound Samples { get; private set; } protected virtual IEnumerable GetSamples() => HitObject.Samples; @@ -154,11 +151,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).ToArray(); - foreach (var s in samples) - s.Namespace = SampleNamespace; - - AddInternal(Samples = new SkinnableSound(samples)); + AddInternal(Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); } private void onDefaultsApplied() => apply(HitObject); diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 7ad93379f0..c38a5c6af7 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -21,12 +21,13 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; +using osu.Game.Users; namespace osu.Game.Rulesets { public abstract class Ruleset { - public readonly RulesetInfo RulesetInfo; + public RulesetInfo RulesetInfo { get; internal set; } public IEnumerable GetAllMods() => Enum.GetValues(typeof(ModType)).Cast() // Confine all mods of each mod type into a single IEnumerable @@ -51,7 +52,14 @@ namespace osu.Game.Rulesets protected Ruleset() { - RulesetInfo = createRulesetInfo(); + RulesetInfo = new RulesetInfo + { + Name = Description, + ShortName = ShortName, + ID = (this as ILegacyRuleset)?.LegacyID, + InstantiationInfo = GetType().AssemblyQualifiedName, + Available = true, + }; } /// @@ -64,10 +72,16 @@ namespace osu.Game.Rulesets public abstract DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null); /// - /// Creates a for a beatmap converted to this ruleset. + /// Creates a for this . /// /// The score processor. - public virtual ScoreProcessor CreateScoreProcessor(IBeatmap beatmap) => new ScoreProcessor(beatmap); + public virtual ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(); + + /// + /// Creates a for this . + /// + /// The health processor. + public virtual HealthProcessor CreateHealthProcessor(double drainStartTime) => new DrainingHealthProcessor(drainStartTime); /// /// Creates a to convert a to one that is applicable for this . @@ -91,7 +105,7 @@ namespace osu.Game.Rulesets public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle }; - public virtual IResourceStore CreateResourceStore() => new NamespacedResourceStore(new DllResourceStore(GetType().Assembly.Location), @"Resources"); + public virtual IResourceStore CreateResourceStore() => new NamespacedResourceStore(new DllResourceStore(GetType().Assembly), @"Resources"); public abstract string Description { get; } @@ -103,16 +117,16 @@ namespace osu.Game.Rulesets /// The to store the settings. public virtual IRulesetConfigManager CreateConfig(SettingsStore settings) => null; - /// - /// Do not override this unless you are a legacy mode. - /// - public virtual int? LegacyID => null; - /// /// A unique short name to reference this ruleset in online requests. /// public abstract string ShortName { get; } + /// + /// The playing verb to be shown in the . + /// + public virtual string PlayingVerb => "Playing solo"; + /// /// A list of available variant ids. /// @@ -138,18 +152,5 @@ namespace osu.Game.Rulesets /// /// An empty frame for the current ruleset, or null if unsupported. public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; - - /// - /// Create a ruleset info based on this ruleset. - /// - /// A filled . - private RulesetInfo createRulesetInfo() => new RulesetInfo - { - Name = Description, - ShortName = ShortName, - InstantiationInfo = GetType().AssemblyQualifiedName, - ID = LegacyID, - Available = true - }; } } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index d695e0b56d..ececc18c96 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -20,11 +20,17 @@ namespace osu.Game.Rulesets [JsonIgnore] public bool Available { get; set; } + // TODO: this should probably be moved to RulesetStore. public virtual Ruleset CreateInstance() { if (!Available) return null; - return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo)); + var ruleset = (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo)); + + // overwrite the pre-populated RulesetInfo with a potentially database attached copy. + ruleset.RulesetInfo = this; + + return ruleset; } 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 5d0c5c7ccf..a389d4ff75 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -58,17 +58,17 @@ namespace osu.Game.Rulesets 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)) + //add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) { - if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) + if (context.RulesetInfo.SingleOrDefault(dbRuleset => dbRuleset.ID == r.RulesetInfo.ID) == null) context.RulesetInfo.Add(r.RulesetInfo); } context.SaveChanges(); //add any other modes - foreach (var r in instances.Where(r => r.LegacyID == null)) + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) context.RulesetInfo.Add(r.RulesetInfo); diff --git a/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs new file mode 100644 index 0000000000..5dfb5167f4 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/AccumulatingHealthProcessor.cs @@ -0,0 +1,32 @@ +// 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.Rulesets.Scoring +{ + /// + /// A that accumulates health and causes a fail if the final health + /// is less than a value required to pass the beatmap. + /// + public class AccumulatingHealthProcessor : HealthProcessor + { + protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value < requiredHealth; + + private readonly double requiredHealth; + + /// + /// Creates a new . + /// + /// The minimum amount of health required to beatmap. + public AccumulatingHealthProcessor(double requiredHealth) + { + this.requiredHealth = requiredHealth; + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + Health.Value = 0; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs new file mode 100644 index 0000000000..fffcbb3c9f --- /dev/null +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.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 System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + /// + /// A which continuously drains health.
+ /// At HP=0, the minimum health reached for a perfect play is 95%.
+ /// At HP=5, the minimum health reached for a perfect play is 70%.
+ /// At HP=10, the minimum health reached for a perfect play is 30%. + ///
+ public class DrainingHealthProcessor : HealthProcessor + { + /// + /// A reasonable allowable error for the minimum health offset from . A 1% error is unnoticeable. + /// + private const double minimum_health_error = 0.01; + + /// + /// The minimum health target at an HP drain rate of 0. + /// + private const double min_health_target = 0.95; + + /// + /// The minimum health target at an HP drain rate of 5. + /// + private const double mid_health_target = 0.70; + + /// + /// The minimum health target at an HP drain rate of 10. + /// + private const double max_health_target = 0.30; + + private IBeatmap beatmap; + + private double gameplayEndTime; + + private readonly double drainStartTime; + + private readonly List<(double time, double health)> healthIncreases = new List<(double, double)>(); + private double targetMinimumHealth; + private double drainRate = 1; + + /// + /// Creates a new . + /// + /// The time after which draining should begin. + public DrainingHealthProcessor(double drainStartTime) + { + this.drainStartTime = drainStartTime; + } + + protected override void Update() + { + base.Update(); + + if (!IsBreakTime.Value) + { + // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time + double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime); + double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime); + + Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime); + } + } + + public override void ApplyBeatmap(IBeatmap beatmap) + { + this.beatmap = beatmap; + + if (beatmap.HitObjects.Count > 0) + gameplayEndTime = beatmap.HitObjects[^1].GetEndTime(); + + targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target); + + base.ApplyBeatmap(beatmap); + } + + protected override void ApplyResultInternal(JudgementResult result) + { + base.ApplyResultInternal(result); + healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); + } + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + drainRate = 1; + + if (storeResults) + drainRate = computeDrainRate(); + + healthIncreases.Clear(); + } + + private double computeDrainRate() + { + if (healthIncreases.Count == 0) + return 0; + + int adjustment = 1; + double result = 1; + + // Although we expect the following loop to converge within 30 iterations (health within 1/2^31 accuracy of the target), + // we'll still keep a safety measure to avoid infinite loops by detecting overflows. + while (adjustment > 0) + { + double currentHealth = 1; + double lowestHealth = 1; + int currentBreak = -1; + + for (int i = 0; i < healthIncreases.Count; i++) + { + double currentTime = healthIncreases[i].time; + double lastTime = i > 0 ? healthIncreases[i - 1].time : drainStartTime; + + // Subtract any break time from the duration since the last object + if (beatmap.Breaks.Count > 0) + { + // Advance the last break occuring before the current time + while (currentBreak + 1 < beatmap.Breaks.Count && beatmap.Breaks[currentBreak + 1].EndTime < currentTime) + currentBreak++; + + if (currentBreak >= 0) + lastTime = Math.Max(lastTime, beatmap.Breaks[currentBreak].EndTime); + } + + // Apply health adjustments + currentHealth -= (healthIncreases[i].time - lastTime) * result; + lowestHealth = Math.Min(lowestHealth, currentHealth); + currentHealth = Math.Min(1, currentHealth + healthIncreases[i].health); + + // Common scenario for when the drain rate is definitely too harsh + if (lowestHealth < 0) + break; + } + + // Stop if the resulting health is within a reasonable offset from the target + if (Math.Abs(lowestHealth - targetMinimumHealth) <= minimum_health_error) + break; + + // This effectively works like a binary search - each iteration the search space moves closer to the target, but may exceed it. + adjustment *= 2; + result += 1.0 / adjustment * Math.Sign(lowestHealth - targetMinimumHealth); + } + + return result; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs new file mode 100644 index 0000000000..0c6b3f67b4 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs @@ -0,0 +1,83 @@ +// 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.Bindables; +using osu.Framework.MathUtils; +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.Scoring +{ + public abstract class HealthProcessor : JudgementProcessor + { + /// + /// Invoked when the is in a failed state. + /// Return true if the fail was permitted. + /// + public event Func Failed; + + /// + /// Additional conditions on top of that cause a failing state. + /// + public event Func FailConditions; + + /// + /// The current health. + /// + public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; + + /// + /// Whether gameplay is currently in a break. + /// + public readonly IBindable IsBreakTime = new Bindable(); + + /// + /// Whether this ScoreProcessor has already triggered the failed state. + /// + public bool HasFailed { get; private set; } + + protected override void ApplyResultInternal(JudgementResult result) + { + result.HealthAtJudgement = Health.Value; + result.FailedAtJudgement = HasFailed; + + if (HasFailed) + return; + + Health.Value += GetHealthIncreaseFor(result); + + if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true) + return; + + if (Failed?.Invoke() != false) + HasFailed = true; + } + + protected override void RevertResultInternal(JudgementResult result) + { + Health.Value = result.HealthAtJudgement; + + // Todo: Revert HasFailed state with proper player support + } + + /// + /// Retrieves the health increase for a . + /// + /// The . + /// The health increase. + protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result); + + /// + /// The default conditions for failing. + /// + protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value); + + protected override void Reset(bool storeResults) + { + base.Reset(storeResults); + + Health.Value = 1; + HasFailed = false; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/JudgementProcessor.cs b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs new file mode 100644 index 0000000000..3016007f98 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/JudgementProcessor.cs @@ -0,0 +1,140 @@ +// 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.Extensions.TypeExtensions; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + public abstract class JudgementProcessor : Component + { + /// + /// Invoked when all s have been judged by this . + /// + public event Action AllJudged; + + /// + /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by this . + /// + public event Action NewJudgement; + + /// + /// The maximum number of hits that can be judged. + /// + protected int MaxHits { get; private set; } + + /// + /// The total number of judged s at the current point in time. + /// + public int JudgedHits { get; private set; } + + /// + /// Whether all s have been processed. + /// + public bool HasCompleted => JudgedHits == MaxHits; + + /// + /// Applies a to this . + /// + /// The to read properties from. + public virtual void ApplyBeatmap(IBeatmap beatmap) + { + Reset(false); + SimulateAutoplay(beatmap); + Reset(true); + } + + /// + /// Applies the score change of a to this . + /// + /// The to apply. + public void ApplyResult(JudgementResult result) + { + JudgedHits++; + + ApplyResultInternal(result); + + NewJudgement?.Invoke(result); + + if (HasCompleted) + AllJudged?.Invoke(); + } + + /// + /// Reverts the score change of a that was applied to this . + /// + /// The judgement scoring result. + public void RevertResult(JudgementResult result) + { + JudgedHits--; + + RevertResultInternal(result); + } + + /// + /// Applies the score change of a to this . + /// + /// + /// Any changes applied via this method can be reverted via . + /// + /// The to apply. + protected abstract void ApplyResultInternal(JudgementResult result); + + /// + /// Reverts the score change of a that was applied to this via . + /// + /// The judgement scoring result. + protected abstract void RevertResultInternal(JudgementResult result); + + /// + /// Resets this to a default state. + /// + /// Whether to store the current state of the for future use. + protected virtual void Reset(bool storeResults) + { + if (storeResults) + MaxHits = JudgedHits; + + JudgedHits = 0; + } + + /// + /// Creates the that represents the scoring result for a . + /// + /// The which was judged. + /// The that provides the scoring information. + protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); + + /// + /// Simulates an autoplay of the to determine scoring values. + /// + /// This provided temporarily. DO NOT USE. + /// The to simulate. + protected virtual void SimulateAutoplay(IBeatmap beatmap) + { + foreach (var obj in beatmap.HitObjects) + simulate(obj); + + void simulate(HitObject obj) + { + foreach (var nested in obj.NestedHitObjects) + simulate(nested); + + var judgement = obj.CreateJudgement(); + if (judgement == null) + return; + + var result = CreateResult(obj, judgement); + if (result == null) + throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + + result.Type = judgement.MaxResult; + ApplyResult(result); + } + } + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index a8a2294498..8ccc2af93b 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,44 +7,18 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.TypeExtensions; -using osu.Framework.MathUtils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Scoring; namespace osu.Game.Rulesets.Scoring { - public class ScoreProcessor + public class ScoreProcessor : JudgementProcessor { private const double base_portion = 0.3; private const double combo_portion = 0.7; private const double max_score = 1000000; - /// - /// Invoked when the is in a failed state. - /// This may occur regardless of whether an event is invoked. - /// Return true if the fail was permitted. - /// - public event Func Failed; - - /// - /// Invoked when all s have been judged. - /// - public event Action AllJudged; - - /// - /// Invoked when a new judgement has occurred. This occurs after the judgement has been processed by the . - /// - public event Action NewJudgement; - - /// - /// Additional conditions on top of that cause a failing state. - /// - public event Func FailConditions; - /// /// The current total score. /// @@ -55,11 +29,6 @@ namespace osu.Game.Rulesets.Scoring ///
public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; - /// - /// The current health. - /// - public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; - /// /// The current combo. /// @@ -85,26 +54,6 @@ namespace osu.Game.Rulesets.Scoring /// public readonly Bindable Mode = new Bindable(); - /// - /// Whether all s have been processed. - /// - public bool HasCompleted => JudgedHits == MaxHits; - - /// - /// Whether this ScoreProcessor has already triggered the failed state. - /// - public bool HasFailed { get; private set; } - - /// - /// The maximum number of hits that can be judged. - /// - protected int MaxHits { get; private set; } - - /// - /// The total number of judged s at the current point in time. - /// - public int JudgedHits { get; private set; } - private double maxHighestCombo; private double maxBaseScore; @@ -114,7 +63,7 @@ namespace osu.Game.Rulesets.Scoring private double scoreMultiplier = 1; - public ScoreProcessor(IBeatmap beatmap) + public ScoreProcessor() { Debug.Assert(base_portion + combo_portion == 1.0); @@ -126,18 +75,6 @@ namespace osu.Game.Rulesets.Scoring Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue); }; - ApplyBeatmap(beatmap); - - Reset(false); - SimulateAutoplay(beatmap); - Reset(true); - - if (maxBaseScore == 0 || maxHighestCombo == 0) - { - Mode.Value = ScoringMode.Classic; - Mode.Disabled = true; - } - Mode.ValueChanged += _ => updateScore(); Mods.ValueChanged += mods => { @@ -150,91 +87,16 @@ namespace osu.Game.Rulesets.Scoring }; } - /// - /// Applies any properties of the which affect scoring to this . - /// - /// The to read properties from. - protected virtual void ApplyBeatmap(IBeatmap beatmap) - { - } - - /// - /// Simulates an autoplay of the to determine scoring values. - /// - /// This provided temporarily. DO NOT USE. - /// The to simulate. - protected virtual void SimulateAutoplay(IBeatmap beatmap) - { - foreach (var obj in beatmap.HitObjects) - simulate(obj); - - void simulate(HitObject obj) - { - foreach (var nested in obj.NestedHitObjects) - simulate(nested); - - var judgement = obj.CreateJudgement(); - if (judgement == null) - return; - - var result = CreateResult(obj, judgement); - if (result == null) - throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - - result.Type = judgement.MaxResult; - - ApplyResult(result); - } - } - - /// - /// Applies the score change of a to this . - /// - /// The to apply. - public void ApplyResult(JudgementResult result) - { - ApplyResultInternal(result); - - updateScore(); - updateFailed(result); - - NewJudgement?.Invoke(result); - - if (HasCompleted) - AllJudged?.Invoke(); - } - - /// - /// Reverts the score change of a that was applied to this . - /// - /// The judgement scoring result. - public void RevertResult(JudgementResult result) - { - RevertResultInternal(result); - updateScore(); - } - private readonly Dictionary scoreResultCounts = new Dictionary(); - /// - /// Applies the score change of a to this . - /// - /// - /// Any changes applied via this method can be reverted via . - /// - /// The to apply. - protected virtual void ApplyResultInternal(JudgementResult result) + protected sealed override void ApplyResultInternal(JudgementResult result) { result.ComboAtJudgement = Combo.Value; result.HighestComboAtJudgement = HighestCombo.Value; - result.HealthAtJudgement = Health.Value; - result.FailedAtJudgement = HasFailed; - if (HasFailed) + if (result.FailedAtJudgement) return; - JudgedHits++; - if (result.Judgement.AffectsCombo) { switch (result.Type) @@ -266,26 +128,17 @@ namespace osu.Game.Rulesets.Scoring rollingMaxBaseScore += result.Judgement.MaxNumericResult; } - Health.Value += HealthAdjustmentFactorFor(result) * result.Judgement.HealthIncreaseFor(result); + updateScore(); } - /// - /// Reverts the score change of a that was applied to this via . - /// - /// The judgement scoring result. - protected virtual void RevertResultInternal(JudgementResult result) + protected sealed override void RevertResultInternal(JudgementResult result) { Combo.Value = result.ComboAtJudgement; HighestCombo.Value = result.HighestComboAtJudgement; - Health.Value = result.HealthAtJudgement; - - // Todo: Revert HasFailed state with proper player support if (result.FailedAtJudgement) return; - JudgedHits--; - if (result.Judgement.IsBonus) { if (result.IsHit) @@ -299,14 +152,9 @@ namespace osu.Game.Rulesets.Scoring baseScore -= result.Judgement.NumericResultFor(result); rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } - } - /// - /// An adjustment factor which is multiplied into the health increase provided by a . - /// - /// The for which the adjustment should apply. - /// The adjustment factor. - protected virtual double HealthAdjustmentFactorFor(JudgementResult result) => 1; + updateScore(); + } private void updateScore() { @@ -330,24 +178,6 @@ namespace osu.Game.Rulesets.Scoring } } - /// - /// Checks if the score is in a failed state and notifies subscribers. - /// - /// This can only ever notify subscribers once. - /// - /// - private void updateFailed(JudgementResult result) - { - if (HasFailed) - return; - - if (!DefaultFailCondition && FailConditions?.Invoke(this, result) != true) - return; - - if (Failed?.Invoke() != false) - HasFailed = true; - } - private ScoreRank rankFrom(double acc) { if (acc == 1) @@ -372,30 +202,33 @@ namespace osu.Game.Rulesets.Scoring /// Resets this ScoreProcessor to a default state. /// /// Whether to store the current state of the for future use. - protected virtual void Reset(bool storeResults) + protected override void Reset(bool storeResults) { + base.Reset(storeResults); + scoreResultCounts.Clear(); if (storeResults) { - MaxHits = JudgedHits; maxHighestCombo = HighestCombo.Value; maxBaseScore = baseScore; + + if (maxBaseScore == 0 || maxHighestCombo == 0) + { + Mode.Value = ScoringMode.Classic; + Mode.Disabled = true; + } } - JudgedHits = 0; baseScore = 0; rollingMaxBaseScore = 0; bonusScore = 0; TotalScore.Value = 0; Accuracy.Value = 1; - Health.Value = 1; Combo.Value = 0; Rank.Value = ScoreRank.X; HighestCombo.Value = 0; - - HasFailed = false; } /// @@ -416,22 +249,10 @@ namespace osu.Game.Rulesets.Scoring score.Statistics[result] = GetStatistic(result); } - /// - /// The default conditions for failing. - /// - protected virtual bool DefaultFailCondition => Precision.AlmostBigger(Health.MinValue, Health.Value); - /// /// Create a for this processor. /// public virtual HitWindows CreateHitWindows() => new HitWindows(); - - /// - /// Creates the that represents the scoring result for a . - /// - /// The which was judged. - /// The that provides the scoring information. - protected virtual JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new JudgementResult(hitObject, judgement); } public enum ScoringMode diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index df1b8078a6..e624fb80fa 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -72,10 +72,9 @@ namespace osu.Game.Rulesets.UI /// public override Playfield Playfield => playfield.Value; - /// - /// Place to put drawables above hit objects but below UI. - /// - public Container Overlays { get; private set; } + private Container overlays; + + public override Container Overlays => overlays; public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; @@ -159,7 +158,7 @@ namespace osu.Game.Rulesets.UI dependencies.Cache(textureStore); localSampleStore = dependencies.Get().GetSampleStore(new NamespacedResourceStore(resources, "Samples")); - dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); + dependencies.CacheAs(new FallbackSampleStore(localSampleStore, dependencies.Get())); } onScreenDisplay = dependencies.Get(); @@ -185,12 +184,15 @@ namespace osu.Game.Rulesets.UI frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime) { FrameStablePlayback = FrameStablePlayback, - Child = KeyBindingInputManager - .WithChild(CreatePlayfieldAdjustmentContainer() - .WithChild(Playfield) - ) + Children = new Drawable[] + { + KeyBindingInputManager + .WithChild(CreatePlayfieldAdjustmentContainer() + .WithChild(Playfield) + ), + overlays = new Container { RelativeSizeAxes = Axes.Both } + } }, - Overlays = new Container { RelativeSizeAxes = Axes.Both } }; if ((ResumeOverlay = CreateResumeOverlay()) != null) @@ -385,6 +387,11 @@ namespace osu.Game.Rulesets.UI /// public abstract Playfield Playfield { get; } + /// + /// Place to put drawables above hit objects but below UI. + /// + public abstract Container Overlays { get; } + /// /// The frame-stable clock which is being used for playfield display. /// diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 857929ff9e..04b4374fc4 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.UI.Scrolling private void computeLifetimeStartRecursive(DrawableHitObject hitObject) { - hitObject.LifetimeStart = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, timeRange.Value); + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); foreach (var obj in hitObject.NestedHitObjects) computeLifetimeStartRecursive(obj); @@ -108,6 +108,35 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) + { + float originAdjustment = 0.0f; + + // calculate the dimension of the part of the hitobject that should already be visible + // when the hitobject origin first appears inside the scrolling container + switch (direction.Value) + { + case ScrollingDirection.Up: + originAdjustment = hitObject.OriginPosition.Y; + break; + + case ScrollingDirection.Down: + originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y; + break; + + case ScrollingDirection.Left: + originAdjustment = hitObject.OriginPosition.X; + break; + + case ScrollingDirection.Right: + originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X; + break; + } + + var adjustedStartTime = scrollingInfo.Algorithm.TimeAt(-originAdjustment, hitObject.HitObject.StartTime, timeRange.Value, scrollLength); + return scrollingInfo.Algorithm.GetDisplayStartTime(adjustedStartTime, timeRange.Value); + } + // Cant use AddOnce() since the delegate is re-constructed every invocation private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 195bc663f1..cafaddc39e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private HitObjectComposer composer { get; set; } [Resolved] - private IEditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } public BlueprintContainer() { diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 00326d04f7..53c5cf97fa 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected IDistanceSnapProvider SnapProvider { get; private set; } [Resolved] - private IEditorBeatmap beatmap { get; set; } + private EditorBeatmap beatmap { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs index db4aca75e5..b20f2fa11d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectDisplay.cs @@ -16,9 +16,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { internal class TimelineHitObjectDisplay : TimelinePart { - private IEditorBeatmap beatmap { get; } + private EditorBeatmap beatmap { get; } - public TimelineHitObjectDisplay(IEditorBeatmap beatmap) + public TimelineHitObjectDisplay(EditorBeatmap beatmap) { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 5d9757778d..1a6aae294a 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() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(composer.EditorBeatmap); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(EditorBeatmap); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 1b4964c068..14d69cddd1 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -23,6 +23,7 @@ using osuTK.Input; using System.Collections.Generic; using osu.Framework; using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Edit.Compose; @@ -49,9 +50,11 @@ namespace osu.Game.Screens.Edit private EditorScreen currentScreen; private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - private EditorClock clock; + private IBeatmap playableBeatmap; + private EditorBeatmap editorBeatmap; + private DependencyContainer dependencies; private GameHost host; @@ -73,9 +76,13 @@ namespace osu.Game.Screens.Edit clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); + playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + editorBeatmap = new EditorBeatmap(playableBeatmap); + dependencies.CacheAs(clock); dependencies.CacheAs(clock); dependencies.Cache(beatDivisor); + dependencies.CacheAs(editorBeatmap); EditorMenuBar menuBar; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index c3a322ea36..6ed74dfdb0 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections; using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -11,30 +12,30 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit { - public class EditorBeatmap : IEditorBeatmap - where T : HitObject + public class EditorBeatmap : IBeatmap { /// - /// Invoked when a is added to this . + /// Invoked when a is added to this . /// public event Action HitObjectAdded; /// - /// Invoked when a is removed from this . + /// Invoked when a is removed from this . /// public event Action HitObjectRemoved; /// - /// Invoked when the start time of a in this was changed. + /// Invoked when the start time of a in this was changed. /// public event Action StartTimeChanged; - private readonly Dictionary> startTimeBindables = new Dictionary>(); - private readonly Beatmap beatmap; + public readonly IBeatmap PlayableBeatmap; - public EditorBeatmap(Beatmap beatmap) + private readonly Dictionary> startTimeBindables = new Dictionary>(); + + public EditorBeatmap(IBeatmap playableBeatmap) { - this.beatmap = beatmap; + PlayableBeatmap = playableBeatmap; foreach (var obj in HitObjects) trackStartTime(obj); @@ -42,82 +43,83 @@ namespace osu.Game.Screens.Edit public BeatmapInfo BeatmapInfo { - get => beatmap.BeatmapInfo; - set => beatmap.BeatmapInfo = value; + get => PlayableBeatmap.BeatmapInfo; + set => PlayableBeatmap.BeatmapInfo = value; } - public BeatmapMetadata Metadata => beatmap.Metadata; + public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo => beatmap.ControlPointInfo; + public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; - public List Breaks => beatmap.Breaks; + public List Breaks => PlayableBeatmap.Breaks; - public double TotalBreakTime => beatmap.TotalBreakTime; + public double TotalBreakTime => PlayableBeatmap.TotalBreakTime; - public IReadOnlyList HitObjects => beatmap.HitObjects; + public IReadOnlyList HitObjects => PlayableBeatmap.HitObjects; - IReadOnlyList IBeatmap.HitObjects => beatmap.HitObjects; + public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); - public IEnumerable GetStatistics() => beatmap.GetStatistics(); + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); - public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); + private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; /// - /// Adds a to this . + /// Adds a to this . /// /// The to add. - public void Add(T hitObject) + public void Add(HitObject hitObject) { trackStartTime(hitObject); // Preserve existing sorting order in the beatmap - var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime); - beatmap.HitObjects.Insert(insertionIndex + 1, hitObject); + var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime); + mutableHitObjects.Insert(insertionIndex + 1, hitObject); HitObjectAdded?.Invoke(hitObject); } /// - /// Removes a from this . + /// Removes a from this . /// /// The to add. - public void Remove(T hitObject) + public void Remove(HitObject hitObject) { - if (beatmap.HitObjects.Remove(hitObject)) - { - var bindable = startTimeBindables[hitObject]; - bindable.UnbindAll(); + if (!mutableHitObjects.Contains(hitObject)) + return; - startTimeBindables.Remove(hitObject); - HitObjectRemoved?.Invoke(hitObject); - } + mutableHitObjects.Remove(hitObject); + + var bindable = startTimeBindables[hitObject]; + bindable.UnbindAll(); + + startTimeBindables.Remove(hitObject); + HitObjectRemoved?.Invoke(hitObject); } - private void trackStartTime(T hitObject) + private void trackStartTime(HitObject hitObject) { startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy(); startTimeBindables[hitObject].ValueChanged += _ => { // For now we'll remove and re-add the hitobject. This is not optimal and can be improved if required. - beatmap.HitObjects.Remove(hitObject); + mutableHitObjects.Remove(hitObject); - var insertionIndex = beatmap.HitObjects.FindLastIndex(h => h.StartTime <= hitObject.StartTime); - beatmap.HitObjects.Insert(insertionIndex + 1, hitObject); + var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime); + mutableHitObjects.Insert(insertionIndex + 1, hitObject); StartTimeChanged?.Invoke(hitObject); }; } - /// - /// Adds a to this . - /// - /// The to add. - public void Add(HitObject hitObject) => Add((T)hitObject); + private int findInsertionIndex(IReadOnlyList list, double startTime) + { + for (int i = 0; i < list.Count; i++) + { + if (list[i].StartTime > startTime) + return i - 1; + } - /// - /// Removes a from this . - /// - /// The to add. - public void Remove(HitObject hitObject) => Remove((T)hitObject); + return list.Count - 1; + } } } diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 1b57c703ae..d42447ac4b 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -17,6 +17,9 @@ namespace osu.Game.Screens.Edit [Resolved] protected IBindable Beatmap { get; private set; } + [Resolved] + protected EditorBeatmap EditorBeatmap { get; private set; } + protected override Container Content => content; private readonly Container content; diff --git a/osu.Game/Screens/Edit/IEditorBeatmap.cs b/osu.Game/Screens/Edit/IEditorBeatmap.cs deleted file mode 100644 index 3e3418ef79..0000000000 --- a/osu.Game/Screens/Edit/IEditorBeatmap.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.Game.Beatmaps; -using osu.Game.Rulesets.Edit; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Screens.Edit -{ - /// - /// Interface for the contained by the see . - /// Children of may resolve the beatmap via or . - /// - public interface IEditorBeatmap : IBeatmap - { - /// - /// Invoked when a is added to this . - /// - event Action HitObjectAdded; - - /// - /// Invoked when a is removed from this . - /// - event Action HitObjectRemoved; - - /// - /// Invoked when the start time of a in this was changed. - /// - event Action StartTimeChanged; - } - - /// - /// Interface for the contained by the see . - /// Children of may resolve the beatmap via or . - /// - public interface IEditorBeatmap : IEditorBeatmap, IBeatmap - where T : HitObject - { - } -} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e2f362780d..236bdc8442 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Play public Bindable ShowHealthbar = new Bindable(true); private readonly ScoreProcessor scoreProcessor; + private readonly HealthProcessor healthProcessor; private readonly DrawableRuleset drawableRuleset; private readonly IReadOnlyList mods; @@ -63,9 +64,10 @@ namespace osu.Game.Screens.Play private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; - public HUDOverlay(ScoreProcessor scoreProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) + public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) { this.scoreProcessor = scoreProcessor; + this.healthProcessor = healthProcessor; this.drawableRuleset = drawableRuleset; this.mods = mods; @@ -119,7 +121,10 @@ namespace osu.Game.Screens.Play private void load(OsuConfigManager config, NotificationOverlay notificationOverlay) { if (scoreProcessor != null) - BindProcessor(scoreProcessor); + BindScoreProcessor(scoreProcessor); + + if (healthProcessor != null) + BindHealthProcessor(healthProcessor); if (drawableRuleset != null) { @@ -288,15 +293,19 @@ namespace osu.Game.Screens.Play protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); - protected virtual void BindProcessor(ScoreProcessor processor) + protected virtual void BindScoreProcessor(ScoreProcessor processor) { ScoreCounter?.Current.BindTo(processor.TotalScore); AccuracyCounter?.Current.BindTo(processor.Accuracy); ComboCounter?.Current.BindTo(processor.Combo); - HealthDisplay?.Current.BindTo(processor.Health); if (HealthDisplay is StandardHealthDisplay shd) processor.NewJudgement += shd.Flash; } + + protected virtual void BindHealthProcessor(HealthProcessor processor) + { + HealthDisplay?.Current.BindTo(processor.Health); + } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8970f9ac88..7228e22382 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -72,6 +72,9 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; protected ScoreProcessor ScoreProcessor { get; private set; } + + protected HealthProcessor HealthProcessor { get; private set; } + protected DrawableRuleset DrawableRuleset { get; private set; } protected HUDOverlay HUDOverlay { get; private set; } @@ -128,9 +131,13 @@ namespace osu.Game.Screens.Play DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); - ScoreProcessor = ruleset.CreateScoreProcessor(playableBeatmap); + ScoreProcessor = ruleset.CreateScoreProcessor(); + ScoreProcessor.ApplyBeatmap(playableBeatmap); ScoreProcessor.Mods.BindTo(Mods); + HealthProcessor = ruleset.CreateHealthProcessor(playableBeatmap.HitObjects[0].StartTime); + HealthProcessor.ApplyBeatmap(playableBeatmap); + if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); @@ -145,15 +152,28 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - DrawableRuleset.OnNewResult += ScoreProcessor.ApplyResult; - DrawableRuleset.OnRevertResult += ScoreProcessor.RevertResult; + DrawableRuleset.OnNewResult += r => + { + HealthProcessor.ApplyResult(r); + ScoreProcessor.ApplyResult(r); + }; - // Bind ScoreProcessor to ourselves + DrawableRuleset.OnRevertResult += r => + { + HealthProcessor.RevertResult(r); + ScoreProcessor.RevertResult(r); + }; + + // Bind the judgement processors to ourselves ScoreProcessor.AllJudged += onCompletion; - ScoreProcessor.Failed += onFail; + HealthProcessor.Failed += onFail; foreach (var mod in Mods.Value.OfType()) mod.ApplyToScoreProcessor(ScoreProcessor); + + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToHealthProcessor(HealthProcessor); + BreakOverlay.IsBreakTime.ValueChanged += _ => updatePauseOnFocusLostState(); } @@ -188,16 +208,10 @@ namespace osu.Game.Screens.Play { target.AddRange(new[] { - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Breaks = working.Beatmap.Breaks - }, // display the cursor above some HUD elements. DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), - HUDOverlay = new HUDOverlay(ScoreProcessor, DrawableRuleset, Mods.Value) + HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) { HoldToQuit = { @@ -248,6 +262,18 @@ namespace osu.Game.Screens.Play }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, } }); + + DrawableRuleset.Overlays.Add(BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Breaks = working.Beatmap.Breaks + }); + + DrawableRuleset.Overlays.Add(ScoreProcessor); + DrawableRuleset.Overlays.Add(HealthProcessor); + + HealthProcessor.IsBreakTime.BindTo(BreakOverlay.IsBreakTime); } private void updatePauseOnFocusLostState() => @@ -342,7 +368,7 @@ namespace osu.Game.Screens.Play private void onCompletion() { // Only show the completion screen if the player hasn't failed - if (ScoreProcessor.HasFailed || completionProgressDelegate != null) + if (HealthProcessor.HasFailed || completionProgressDelegate != null) return; ValidForResume = false; @@ -350,18 +376,7 @@ namespace osu.Game.Screens.Play if (!showResults) return; using (BeginDelayedSequence(1000)) - { - completionProgressDelegate = Schedule(delegate - { - if (!this.IsCurrentScreen()) return; - - var score = CreateScore(); - if (DrawableRuleset.ReplayScore == null) - scoreManager.Import(score).Wait(); - - this.Push(CreateResults(score)); - }); - } + scheduleGotoRanking(); } protected virtual ScoreInfo CreateScore() @@ -542,7 +557,7 @@ namespace osu.Game.Screens.Play if (completionProgressDelegate != null && !completionProgressDelegate.Cancelled && !completionProgressDelegate.Completed) { // proceed to result screen if beatmap already finished playing - completionProgressDelegate.RunTask(); + scheduleGotoRanking(); return true; } @@ -577,6 +592,19 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = false; } + private void scheduleGotoRanking() + { + completionProgressDelegate?.Cancel(); + completionProgressDelegate = Schedule(delegate + { + var score = CreateScore(); + if (DrawableRuleset.ReplayScore == null) + scoreManager.Import(score).Wait(); + + this.Push(CreateResults(score)); + }); + } + #endregion } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 57021dfc68..64fcc48004 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -118,8 +118,6 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750) }); - - loadNewPlayer(); } protected override void LoadComplete() @@ -127,6 +125,21 @@ namespace osu.Game.Screens.Play base.LoadComplete(); inputManager = GetContainingInputManager(); + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + loadNewPlayer(); + + content.ScaleTo(0.7f); + Background?.FadeColour(Color4.White, 800, Easing.OutQuint); + + contentIn(); + + info.Delay(750).FadeIn(500); + this.Delay(1800).Schedule(pushWhenLoaded); if (!muteWarningShownOnce.Value) { @@ -179,19 +192,6 @@ namespace osu.Game.Screens.Play content.FadeOut(250); } - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - content.ScaleTo(0.7f); - Background?.FadeColour(Color4.White, 800, Easing.OutQuint); - - contentIn(); - - info.Delay(750).FadeIn(500); - this.Delay(1800).Schedule(pushWhenLoaded); - } - protected override void LogoArriving(OsuLogo logo, bool resuming) { base.LogoArriving(logo, resuming); diff --git a/osu.Game/Screens/Play/ReplayPlayerLoader.cs b/osu.Game/Screens/Play/ReplayPlayerLoader.cs index c8ca604902..4572570437 100644 --- a/osu.Game/Screens/Play/ReplayPlayerLoader.cs +++ b/osu.Game/Screens/Play/ReplayPlayerLoader.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Game.Scoring; namespace osu.Game.Screens.Play @@ -20,15 +20,13 @@ namespace osu.Game.Screens.Play scoreInfo = score.ScoreInfo; } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + public override void OnEntering(IScreen last) { - var dependencies = base.CreateChildDependencies(parent); - // these will be reverted thanks to PlayerLoader's lease. Mods.Value = scoreInfo.Mods; Ruleset.Value = scoreInfo.Ruleset; - return dependencies; + base.OnEntering(last); } } } diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index 433e8ee398..19ecdb6dbf 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -48,6 +48,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, + IsSwitchable = true, }, }, modsCheckbox = new OsuTabControlCheckbox diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 9c9c33274f..a147527f6c 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -16,6 +16,9 @@ using osu.Framework.Bindables; using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; +using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Select.Details { @@ -69,7 +72,37 @@ namespace osu.Game.Screens.Select.Details { base.LoadComplete(); - mods.BindValueChanged(_ => updateStatistics(), true); + mods.BindValueChanged(modsChanged, true); + } + + private readonly List references = new List(); + + private void modsChanged(ValueChangedEvent> mods) + { + // TODO: find a more permanent solution for this if/when it is needed in other components. + // this is generating drawables for the only purpose of storing bindable references. + foreach (var r in references) + r.Dispose(); + + references.Clear(); + + ScheduledDelegate debounce = null; + + foreach (var mod in mods.NewValue.OfType()) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => + { + debounce?.Cancel(); + debounce = Scheduler.AddDelayed(updateStatistics, 100); + }; + + references.Add(setting); + } + } + + updateStatistics(); } private void updateStatistics() diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 48c520986a..671d37fda4 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -175,7 +175,7 @@ namespace osu.Game.Skinning { foreach (var lookup in sampleInfo.LookupNames) { - var sample = Samples?.Get(getFallbackName(lookup)); + var sample = Samples?.Get(lookup); if (sample != null) return sample; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 18dbd212cc..8926c76018 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual } [Resolved] - private AudioManager audio { get; set; } + protected AudioManager Audio { get; private set; } protected virtual IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual CreateWorkingBeatmap(CreateBeatmap(ruleset), null); protected virtual WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => - new ClockBackedTestWorkingBeatmap(beatmap, storyboard, Clock, audio); + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, Clock, Audio); [BackgroundDependencyLoader] private void load(RulesetStore rulesets) diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 8030fc55a2..3c9f201805 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Rulesets; using osuTK.Graphics; namespace osu.Game.Users @@ -44,15 +45,15 @@ namespace osu.Game.Users { public BeatmapInfo Beatmap { get; } - public Rulesets.RulesetInfo Ruleset { get; } + public RulesetInfo Ruleset { get; } - public SoloGame(BeatmapInfo info, Rulesets.RulesetInfo ruleset) + public SoloGame(BeatmapInfo info, RulesetInfo ruleset) { Beatmap = info; Ruleset = ruleset; } - public override string Status => @"Playing alone"; + public override string Status => Ruleset.CreateInstance().PlayingVerb; } public class Spectating : UserActivity diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 4c5f3e0f38..565608b40f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/osu.iOS.props b/osu.iOS.props index ccfbee9a62..60355b8592 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -73,8 +73,8 @@ - - + + @@ -82,7 +82,7 @@ - + diff --git a/osu.iOS/Info.plist b/osu.iOS/Info.plist index 5ceccdf99f..249474b1d7 100644 --- a/osu.iOS/Info.plist +++ b/osu.iOS/Info.plist @@ -14,8 +14,6 @@ 0.1.0 LSRequiresIPhoneOS - LSSupportsOpeningDocumentsInPlace - MinimumOSVersion 10.0 UIDeviceFamily @@ -23,6 +21,8 @@ 1 2 + UIFileSharingEnabled + UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities @@ -51,7 +51,7 @@ UTTypeConformsTo - + public.data UTTypeIdentifier sh.ppy.osu.items @@ -105,6 +105,8 @@ Owner CFBundleTypeName Supported osu! files + CFBundleTypeRole + Viewer LSItemContentTypes sh.ppy.osu.items