diff --git a/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStage.cs b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStage.cs new file mode 100644 index 000000000..51a2c2a5e --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStage.cs @@ -0,0 +1,12 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Karaoke.Stages.Infos; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Rulesets.Karaoke.Mods; + +public interface IApplicableToStage : IApplicableMod +{ + bool CanApply(StageInfo stageInfo); +} diff --git a/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageElement.cs b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageElement.cs new file mode 100644 index 000000000..2869b186b --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageElement.cs @@ -0,0 +1,12 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Karaoke.Stages; + +namespace osu.Game.Rulesets.Karaoke.Mods; + +public interface IApplicableToStageElement : IApplicableToStage +{ + IEnumerable PostProcess(IEnumerable stageElements); +} diff --git a/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageHitObjectCommand.cs b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageHitObjectCommand.cs new file mode 100644 index 000000000..7a8db2ac8 --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageHitObjectCommand.cs @@ -0,0 +1,17 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Karaoke.Stages.Commands; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Karaoke.Mods; + +public interface IApplicableToStageHitObjectCommand : IApplicableToStage +{ + IEnumerable PostProcessInitialCommands(HitObject hitObject, IEnumerable commands); + + IEnumerable PostProcessStartTimeStateCommands(HitObject hitObject, IEnumerable commands); + + IEnumerable PostProcessHitStateCommands(HitObject hitObject, IEnumerable commands); +} diff --git a/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageInfo.cs b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageInfo.cs new file mode 100644 index 000000000..9ddd7b5f7 --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStageInfo.cs @@ -0,0 +1,18 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Karaoke.Beatmaps; +using osu.Game.Rulesets.Karaoke.Stages.Infos; + +namespace osu.Game.Rulesets.Karaoke.Mods; + +/// +/// An interface for mods that prefer to use the type of . +/// Also, it can override the parameter of . +/// +public interface IApplicableToStageInfo : IApplicableToStage +{ + StageInfo? CreateDefaultStageInfo(KaraokeBeatmap beatmap); + + void ApplyToStageInfo(StageInfo stageInfo); +} diff --git a/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStagePlayfieldCommand.cs b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStagePlayfieldCommand.cs new file mode 100644 index 000000000..8eb6e7187 --- /dev/null +++ b/osu.Game.Rulesets.Karaoke/Mods/IApplicableToStagePlayfieldCommand.cs @@ -0,0 +1,13 @@ +// Copyright (c) andy840119 . Licensed under the GPL Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Karaoke.Stages.Commands; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Karaoke.Mods; + +public interface IApplicableToStagePlayfieldCommand : IApplicableToStage +{ + IEnumerable PostProcessCommands(Playfield playfield, IEnumerable commands); +} diff --git a/osu.Game.Rulesets.Karaoke/Mods/KaraokeModClassicStage.cs b/osu.Game.Rulesets.Karaoke/Mods/KaraokeModClassicStage.cs index 7dfb03a95..2c9984df8 100644 --- a/osu.Game.Rulesets.Karaoke/Mods/KaraokeModClassicStage.cs +++ b/osu.Game.Rulesets.Karaoke/Mods/KaraokeModClassicStage.cs @@ -24,7 +24,7 @@ protected override ClassicStageInfo CreateStageInfo(KaraokeBeatmap beatmap) return (ClassicStageInfo)generator.Generate(beatmap); } - protected override void ApplyToCurrentStageInfo(ClassicStageInfo stageInfo) + protected override void ApplyToStageInfo(ClassicStageInfo stageInfo) { // todo: adjust stage by config. } diff --git a/osu.Game.Rulesets.Karaoke/Mods/KaraokeModPreviewStage.cs b/osu.Game.Rulesets.Karaoke/Mods/KaraokeModPreviewStage.cs index 4517a65f2..cd35a430f 100644 --- a/osu.Game.Rulesets.Karaoke/Mods/KaraokeModPreviewStage.cs +++ b/osu.Game.Rulesets.Karaoke/Mods/KaraokeModPreviewStage.cs @@ -24,7 +24,7 @@ protected override PreviewStageInfo CreateStageInfo(KaraokeBeatmap beatmap) return (PreviewStageInfo)generator.Generate(beatmap); } - protected override void ApplyToCurrentStageInfo(PreviewStageInfo stageInfo) + protected override void ApplyToStageInfo(PreviewStageInfo stageInfo) { // todo: adjust stage by config. } diff --git a/osu.Game.Rulesets.Karaoke/Mods/ModStage.cs b/osu.Game.Rulesets.Karaoke/Mods/ModStage.cs index 5e0b3ad56..6b8e998b4 100644 --- a/osu.Game.Rulesets.Karaoke/Mods/ModStage.cs +++ b/osu.Game.Rulesets.Karaoke/Mods/ModStage.cs @@ -3,56 +3,43 @@ using System; using System.Linq; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Karaoke.Mods; -public abstract class ModStage : ModStage +public abstract class ModStage : Mod, IApplicableToStageInfo where TStageInfo : StageInfo { - public override bool IsStageInfoMatched(StageInfo stageInfo) + public sealed override ModType Type => ModType.Conversion; + + /// + /// Change the stage type should not affect the score. + /// + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => new[] { typeof(ModStage) }.Except(new[] { GetType() }).ToArray(); + + public bool CanApply(StageInfo stageInfo) { - return stageInfo is TStageInfo; + return stageInfo is TStageInfo; } - public override StageInfo GenerateDefaultStageInfo(IBeatmap beatmap) + public StageInfo? CreateDefaultStageInfo(KaraokeBeatmap beatmap) { - if (beatmap is not KaraokeBeatmap karaokeBeatmap) - throw new InvalidOperationException(); - - return CreateStageInfo(karaokeBeatmap) ?? throw new InvalidOperationException(); + return CreateStageInfo(beatmap); } - public override void ApplyToStageInfo(StageInfo stageInfo) + public void ApplyToStageInfo(StageInfo stageInfo) { if (stageInfo is not TStageInfo tStageInfo) - throw new InvalidOperationException(); + throw new ArgumentException($"The stage info is not matched with {GetType().Name}"); - ApplyToCurrentStageInfo(tStageInfo); + ApplyToStageInfo(tStageInfo); } - protected abstract void ApplyToCurrentStageInfo(TStageInfo stageInfo); - protected abstract TStageInfo? CreateStageInfo(KaraokeBeatmap beatmap); -} - -public abstract class ModStage : Mod, IApplicableMod -{ - public sealed override ModType Type => ModType.Conversion; - - /// - /// Change the stage type should not affect the score. - /// - public override double ScoreMultiplier => 1; - - public override Type[] IncompatibleMods => new[] { typeof(ModStage) }.Except(new[] { GetType() }).ToArray(); - - public abstract bool IsStageInfoMatched(StageInfo stageInfo); - - public abstract StageInfo GenerateDefaultStageInfo(IBeatmap beatmap); - public abstract void ApplyToStageInfo(StageInfo stageInfo); + protected abstract void ApplyToStageInfo(TStageInfo stageInfo); } diff --git a/osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStage.cs b/osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStage.cs index e5b302963..f61591c75 100644 --- a/osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStage.cs +++ b/osu.Game.Rulesets.Karaoke/Stages/Drawables/DrawableStage.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Rulesets.Karaoke.Beatmaps; +using osu.Game.Rulesets.Karaoke.Edit.Generator.Stages.Preview; using osu.Game.Rulesets.Karaoke.Mods; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.Stages.Infos.Preview; @@ -77,16 +78,39 @@ private static StageInfo getStageInfo(IReadOnlyList mods, KaraokeBeatmap be // todo: get all available stages from resource provider. var availableStageInfos = Array.Empty(); - var stageMod = mods.OfType().SingleOrDefault(); + // Get list of matched mods. + // Return the first stage info if no stage mod is found. + var stageMod = mods.OfType().SingleOrDefault(); if (stageMod == null) - return availableStageInfos.FirstOrDefault() ?? new PreviewStageInfo(); + return availableStageInfos.FirstOrDefault() ?? createDefaultStageInfo(beatmap); - var matchedStageInfo = availableStageInfos.FirstOrDefault(x => stageMod.IsStageInfoMatched(x)); + // If user select a stage mod, means user want to use the specific type of stage. + // We should find the matched stage info from the available stage infos. + var matchedStageInfo = availableStageInfos.FirstOrDefault(x => stageMod.CanApply(x)); + // If the matched stage info is not found, then trying to create a default one. if (matchedStageInfo == null) - matchedStageInfo = stageMod.GenerateDefaultStageInfo(beatmap); + { + // Note that not every stage mod can create the default stage info. + // If not possible to create, then use the default one and not override the value in the stage info. + var newStageInfo = stageMod.CreateDefaultStageInfo(beatmap); + if (newStageInfo == null) + { + return createDefaultStageInfo(beatmap); + } + + matchedStageInfo = newStageInfo; + } stageMod.ApplyToStageInfo(matchedStageInfo); return matchedStageInfo; } + + private static StageInfo createDefaultStageInfo(KaraokeBeatmap beatmap) + { + var config = new PreviewStageInfoGeneratorConfig(); + var generator = new PreviewStageInfoGenerator(config); + + return (PreviewStageInfo)generator.Generate(beatmap); + } } diff --git a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageElementRunner.cs b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageElementRunner.cs index 3700788b7..15055cb0f 100644 --- a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageElementRunner.cs +++ b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageElementRunner.cs @@ -1,8 +1,11 @@ // Copyright (c) andy840119 . Licensed under the GPL 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.Graphics.Containers; +using osu.Game.Rulesets.Karaoke.Mods; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Mods; @@ -11,11 +14,13 @@ namespace osu.Game.Rulesets.Karaoke.Stages.Drawables; public class StageElementRunner : StageRunner, IStageElementRunner { private IStageElementProvider? elementProvider; + private IList? stageMods; private Container? elementContainer; public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList mods) { elementProvider = stageInfo.CreateStageElementProvider(scorable); + stageMods = mods.OfType().Where(x => x.CanApply(stageInfo)).ToList(); applyTransforms(); } @@ -32,12 +37,25 @@ public void UpdateStageElements(Container container) private void applyTransforms() { - if (elementProvider == null || elementContainer == null) + if (elementContainer == null) return; elementContainer.Clear(); - foreach (var element in elementProvider.GetElements()) + foreach (var element in getCommand()) elementContainer.Add(element.CreateDrawable()); } + + private IEnumerable getCommand() + { + if (elementProvider == null) + return Array.Empty(); + + var commands = elementProvider.GetElements(); + + if (stageMods == null) + return commands; + + return stageMods.Aggregate(commands, (current, mod) => mod.PostProcess(current)); + } } diff --git a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageHitObjectRunner.cs b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageHitObjectRunner.cs index 233ab1a0c..2452b7659 100644 --- a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageHitObjectRunner.cs +++ b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StageHitObjectRunner.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Rulesets.Karaoke.Mods; using osu.Game.Rulesets.Karaoke.Objects; using osu.Game.Rulesets.Karaoke.Stages.Commands; using osu.Game.Rulesets.Karaoke.Stages.Infos; @@ -18,10 +19,13 @@ public class StageHitObjectRunner : StageRunner, IStageHitObjectRunner public event Action? OnCommandUpdated; private IHitObjectCommandProvider commandProvider = null!; + private IList stageMods = null!; public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList mods) { commandProvider = stageInfo.CreateHitObjectCommandProvider()!; + stageMods = mods.OfType().Where(x => x.CanApply(stageInfo)).ToList(); + OnCommandUpdated?.Invoke(); } @@ -48,6 +52,8 @@ public double GetEndTimeOffset(HitObject hitObject) public void UpdateInitialTransforms(DrawableHitObject drawableHitObject) { var commands = commandProvider.GetInitialCommands(drawableHitObject.HitObject); + + commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessInitialCommands); applyTransforms(drawableHitObject, commands); } @@ -55,6 +61,8 @@ public void UpdateStartTimeStateTransforms(DrawableHitObject drawableHitObject) { var commands = commandProvider.GetStartTimeStateCommands(drawableHitObject.HitObject); double startTimeOffset = -commandProvider.GetStartTimeOffset(drawableHitObject.HitObject); + + commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessStartTimeStateCommands); applyTransforms(drawableHitObject, commands, startTimeOffset); } @@ -64,9 +72,19 @@ public void UpdateHitStateTransforms(DrawableHitObject drawableHitObject, ArmedS return; var commands = commandProvider.GetHitStateCommands(drawableHitObject.HitObject, state); + + commands = postProcessCommand(drawableHitObject.HitObject, commands, x => x.PostProcessHitStateCommands); applyTransforms(drawableHitObject, commands); } + private IEnumerable postProcessCommand( + HitObject hitObject, + IEnumerable commands, + Func, IEnumerable>> postProcess) + { + return stageMods.Aggregate(commands, (current, mod) => postProcess(mod).Invoke(hitObject, current)); + } + private static void applyTransforms(TDrawable drawable, IEnumerable commands, double offset = 0) where TDrawable : DrawableHitObject { diff --git a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StagePlayfieldRunner.cs b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StagePlayfieldRunner.cs index 15bcf2529..cf6742449 100644 --- a/osu.Game.Rulesets.Karaoke/Stages/Drawables/StagePlayfieldRunner.cs +++ b/osu.Game.Rulesets.Karaoke/Stages/Drawables/StagePlayfieldRunner.cs @@ -1,8 +1,10 @@ // Copyright (c) andy840119 . Licensed under the GPL Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; +using osu.Game.Rulesets.Karaoke.Mods; using osu.Game.Rulesets.Karaoke.Stages.Commands; using osu.Game.Rulesets.Karaoke.Stages.Infos; using osu.Game.Rulesets.Karaoke.UI; @@ -14,11 +16,13 @@ namespace osu.Game.Rulesets.Karaoke.Stages.Drawables; public class StagePlayfieldRunner : StageRunner, IStagePlayfieldRunner { private IPlayfieldCommandProvider? commandProvider; + private IList? stageMods; private KaraokePlayfield? karaokePlayfield; public override void OnStageInfoChanged(StageInfo stageInfo, bool scorable, IReadOnlyList mods) { commandProvider = stageInfo.CreatePlayfieldCommandProvider(scorable); + stageMods = mods.OfType().Where(x => x.CanApply(stageInfo)).ToList(); applyTransforms(); } @@ -36,15 +40,29 @@ public void UpdatePlayfieldTransforms(KaraokePlayfield playfield) private void applyTransforms() { - if (commandProvider == null || karaokePlayfield == null) + if (karaokePlayfield == null) return; var lyricPlayfield = karaokePlayfield.LyricPlayfield; var notePlayfield = karaokePlayfield.NotePlayfield; - applyTransforms(karaokePlayfield, commandProvider.GetCommands(karaokePlayfield)); - applyTransforms(lyricPlayfield, commandProvider.GetCommands(lyricPlayfield)); - applyTransforms(notePlayfield, commandProvider.GetCommands(notePlayfield)); + applyTransforms(karaokePlayfield, getCommand(karaokePlayfield)); + applyTransforms(lyricPlayfield, getCommand(lyricPlayfield)); + applyTransforms(notePlayfield, getCommand(notePlayfield)); + } + + private IEnumerable getCommand( + Playfield playfield) + { + if (commandProvider == null) + return Array.Empty(); + + var commands = commandProvider.GetCommands(playfield); + + if (stageMods == null) + return commands; + + return stageMods.Aggregate(commands, (current, mod) => mod.PostProcessCommands(playfield, current)); } private static void applyTransforms(TDrawable drawable, IEnumerable commands)