1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-06 06:57:39 +08:00

Add ruleset-specific legacy skin providers

This moves implementation of osu! skinnables to OsuLegacySkin.
This commit is contained in:
Dean Herbert 2019-08-26 12:21:49 +09:00
parent de2c6aa23d
commit 5e362d10b1
6 changed files with 261 additions and 153 deletions

View File

@ -0,0 +1,228 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu
{
public class OsuLegacySkin : ISkin
{
private readonly ISkin source;
private Lazy<SkinConfiguration> configuration;
private Lazy<bool> hasHitCircle;
/// <summary>
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
/// Their hittable area is 128px, but the actual circle portion is 118px.
/// We must account for some gameplay elements such as slider bodies, where this padding is not present.
/// </summary>
private const float legacy_circle_radius = 64 - 5;
public OsuLegacySkin(ISkinSource source)
{
this.source = source;
source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
configuration = new Lazy<SkinConfiguration>(() =>
{
var config = new SkinConfiguration();
if (hasHitCircle.Value)
config.SliderPathRadius = legacy_circle_radius;
return config;
});
hasHitCircle = new Lazy<bool>(() => source.GetTexture("hitcircle") != null);
}
private const double default_frame_time = 1000 / 60d;
public Drawable GetDrawableComponent(string componentName)
{
switch (componentName)
{
case "Play/osu/sliderball":
var sliderBallContent = getAnimation("sliderb", true, true, "");
if (sliderBallContent != null)
{
var size = sliderBallContent.Size;
sliderBallContent.RelativeSizeAxes = Axes.Both;
sliderBallContent.Size = Vector2.One;
return new LegacySliderBall(sliderBallContent)
{
Size = size
};
}
return null;
case "Play/osu/hitcircle":
if (hasHitCircle.Value)
return new LegacyMainCirclePiece();
return null;
}
return null;
}
private Drawable getAnimation(string componentName, bool animatable, bool looping, string animationSeparator = "-")
{
Texture texture;
Texture getFrameTexture(int frame) => source.GetTexture($"{componentName}{animationSeparator}{frame}");
TextureAnimation animation = null;
if (animatable)
{
for (int i = 0;; i++)
{
if ((texture = getFrameTexture(i)) == null)
break;
if (animation == null)
animation = new TextureAnimation
{
DefaultFrameLength = default_frame_time,
Repeat = looping
};
animation.AddFrame(texture);
}
}
if (animation != null)
return animation;
if ((texture = source.GetTexture(componentName)) != null)
return new Sprite { Texture = texture };
return null;
}
public Texture GetTexture(string componentName) => null;
public SampleChannel GetSample(string sampleName) => null;
public TValue GetValue<TConfiguration, TValue>(Func<TConfiguration, TValue> query) where TConfiguration : SkinConfiguration
=> configuration.Value is TConfiguration conf ? query.Invoke(conf) : default;
public class LegacySliderBall : CompositeDrawable
{
private readonly Drawable animationContent;
public LegacySliderBall(Drawable animationContent)
{
this.animationContent = animationContent;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
animationContent.Colour = skin.GetValue<SkinConfiguration, Color4?>(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White;
InternalChildren = new[]
{
new Sprite
{
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
animationContent,
new Sprite
{
Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive,
},
};
}
}
public class LegacyMainCirclePiece : CompositeDrawable
{
public LegacyMainCirclePiece()
{
Size = new Vector2(128);
}
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
Sprite hitCircleSprite;
InternalChildren = new Drawable[]
{
hitCircleSprite = new Sprite
{
Texture = skin.GetTexture("hitcircle"),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
}, confineMode: ConfineMode.NoScaling)
{
Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
},
new Sprite
{
Texture = skin.GetTexture("hitcircleoverlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
}
private void updateState(ValueChangedEvent<ArmedState> state)
{
const double legacy_fade_duration = 240;
switch (state.NewValue)
{
case ArmedState.Hit:
this.FadeOut(legacy_fade_duration, Easing.Out);
this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
break;
}
}
}
}
}

View File

@ -24,6 +24,7 @@ using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Osu
{
@ -163,6 +164,8 @@ namespace osu.Game.Rulesets.Osu
public override RulesetSettingsSubsection CreateSettings() => new OsuSettingsSubsection(this);
public override ISkin CreateLegacySkinProvider(ISkinSource source) => new OsuLegacySkin(source);
public override int? LegacyID => 0;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.UI
},
// Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal
// Todo: Remove when hitobjects are properly pooled
new LocalSkinOverrideContainer(null)
new SkinProvidingContainer(null)
{
RelativeSizeAxes = Axes.Both,
Child = HitObjectContainer,

View File

@ -18,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets
{
@ -44,6 +45,8 @@ namespace osu.Game.Rulesets
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().First();
public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null;
protected Ruleset(RulesetInfo rulesetInfo = null)
{
RulesetInfo = rulesetInfo ?? createRulesetInfo();

View File

@ -60,7 +60,9 @@ namespace osu.Game.Screens.Play
[Resolved]
private ScoreManager scoreManager { get; set; }
private RulesetInfo ruleset;
private RulesetInfo rulesetInfo;
private Ruleset ruleset;
private IAPIProvider api;
@ -121,20 +123,29 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = new GameplayClockContainer(working, Mods.Value, DrawableRuleset.GameplayStartTime);
SkinProvidingContainer skinProvidingContainer = new BeatmapSkinProvidingContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
};
GameplayClockContainer.Children = new[]
{
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both },
new ScalingContainer(ScalingMode.Gameplay)
{
Child = new LocalSkinOverrideContainer(working.Skin)
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Child = skinProvidingContainer.WithChild(
// the skinProvidingContainer is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources.
new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(skinProvidingContainer))
{
DrawableRuleset,
new ComboEffects(ScoreProcessor)
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
DrawableRuleset,
new ComboEffects(ScoreProcessor)
}
}
}
)
},
breakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor)
{
@ -222,20 +233,20 @@ namespace osu.Game.Screens.Play
if (beatmap == null)
throw new InvalidOperationException("Beatmap was not loaded");
ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
var rulesetInstance = ruleset.CreateInstance();
rulesetInfo = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
try
{
DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(working, Mods.Value);
DrawableRuleset = ruleset.CreateDrawableRulesetWith(working, Mods.Value);
}
catch (BeatmapInvalidForRulesetException)
{
// we may fail to create a DrawableRuleset if the beatmap cannot be loaded with the user's preferred ruleset
// let's try again forcing the beatmap's ruleset.
ruleset = beatmap.BeatmapInfo.Ruleset;
rulesetInstance = ruleset.CreateInstance();
DrawableRuleset = rulesetInstance.CreateDrawableRulesetWith(Beatmap.Value, Mods.Value);
rulesetInfo = beatmap.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
DrawableRuleset = ruleset.CreateDrawableRulesetWith(Beatmap.Value, Mods.Value);
}
if (!DrawableRuleset.Objects.Any())
@ -313,7 +324,7 @@ namespace osu.Game.Screens.Play
var score = DrawableRuleset.ReplayScore?.ScoreInfo ?? new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = ruleset,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
User = api.LocalUser.Value,
};

View File

@ -9,7 +9,6 @@ using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
@ -20,8 +19,6 @@ using osu.Framework.Text;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osuTK;
using osuTK.Graphics;
@ -34,13 +31,6 @@ namespace osu.Game.Skinning
protected IResourceStore<SampleChannel> Samples;
/// <summary>
/// On osu-stable, hitcircles have 5 pixels of transparent padding on each side to allow for shadows etc.
/// Their hittable area is 128px, but the actual circle portion is 118px.
/// We must account for some gameplay elements such as slider bodies, where this padding is not present.
/// </summary>
private const float legacy_circle_radius = 64 - 5;
public LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager)
: this(skin, new LegacySkinResourceStore<SkinFileInfo>(skin, storage), audioManager, "skin.ini")
{
@ -48,8 +38,6 @@ namespace osu.Game.Skinning
if (!Configuration.CustomColours.ContainsKey("SliderBall")) Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255);
}
private readonly bool hasHitCircle;
protected LegacySkin(SkinInfo skin, IResourceStore<byte[]> storage, AudioManager audioManager, string filename)
: base(skin)
{
@ -62,14 +50,6 @@ namespace osu.Game.Skinning
Samples = audioManager.GetSampleStore(storage);
Textures = new TextureStore(new TextureLoaderStore(storage));
using (var testStream = storage.GetStream("hitcircle@2x") ?? storage.GetStream("hitcircle"))
hasHitCircle |= testStream != null;
if (hasHitCircle)
{
Configuration.SliderPathRadius = legacy_circle_radius;
}
}
protected override void Dispose(bool isDisposing)
@ -94,30 +74,6 @@ namespace osu.Game.Skinning
return null;
case "Play/osu/sliderball":
var sliderBallContent = getAnimation("sliderb", true, true, "");
if (sliderBallContent != null)
{
var size = sliderBallContent.Size;
sliderBallContent.RelativeSizeAxes = Axes.Both;
sliderBallContent.Size = Vector2.One;
return new LegacySliderBall(sliderBallContent)
{
Size = size
};
}
return null;
case "Play/osu/hitcircle":
if (hasHitCircle)
return new LegacyMainCirclePiece();
return null;
case "Play/osu/sliderfollowcircle":
animatable = true;
break;
@ -355,99 +311,6 @@ namespace osu.Game.Skinning
}
}
public class LegacySliderBall : CompositeDrawable
{
private readonly Drawable animationContent;
public LegacySliderBall(Drawable animationContent)
{
this.animationContent = animationContent;
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
animationContent.Colour = skin.GetValue<SkinConfiguration, Color4?>(s => s.CustomColours.ContainsKey("SliderBall") ? s.CustomColours["SliderBall"] : (Color4?)null) ?? Color4.White;
InternalChildren = new[]
{
new Sprite
{
Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255),
},
animationContent,
new Sprite
{
Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive,
},
};
}
}
public class LegacyMainCirclePiece : CompositeDrawable
{
public LegacyMainCirclePiece()
{
Size = new Vector2(128);
}
private readonly IBindable<ArmedState> state = new Bindable<ArmedState>();
private readonly Bindable<Color4> accentColour = new Bindable<Color4>();
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
{
Sprite hitCircleSprite;
InternalChildren = new Drawable[]
{
hitCircleSprite = new Sprite
{
Texture = skin.GetTexture("hitcircle"),
Colour = drawableObject.AccentColour.Value,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new SkinnableSpriteText("Play/osu/number-text", _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
}, confineMode: ConfineMode.NoScaling)
{
Text = (((IHasComboInformation)drawableObject.HitObject).IndexInCurrentCombo + 1).ToString()
},
new Sprite
{
Texture = skin.GetTexture("hitcircleoverlay"),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
};
state.BindTo(drawableObject.State);
state.BindValueChanged(updateState, true);
accentColour.BindTo(drawableObject.AccentColour);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true);
}
private void updateState(ValueChangedEvent<ArmedState> state)
{
const double legacy_fade_duration = 240;
switch (state.NewValue)
{
case ArmedState.Hit:
this.FadeOut(legacy_fade_duration, Easing.Out);
this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
break;
}
}
}
/// <summary>
/// A sprite which is displayed within the playfield, but historically was not considered part of the playfield.
/// Performs scale adjustment to undo the scale applied by <see cref="PlayfieldAdjustmentContainer"/> (osu! ruleset specifically).