mirror of
https://github.com/osukey/osukey.git
synced 2025-05-14 10:07:36 +09:00
Merge pull request #1278 from Damnae/storyboard_integration
Storyboard integration
This commit is contained in:
commit
3fd4a97a19
@ -54,6 +54,7 @@ namespace osu.Game.Configuration
|
|||||||
// Graphics
|
// Graphics
|
||||||
Set(OsuSetting.ShowFpsDisplay, false);
|
Set(OsuSetting.ShowFpsDisplay, false);
|
||||||
|
|
||||||
|
Set(OsuSetting.ShowStoryboard, true);
|
||||||
Set(OsuSetting.CursorRotation, true);
|
Set(OsuSetting.CursorRotation, true);
|
||||||
|
|
||||||
Set(OsuSetting.MenuParallax, true);
|
Set(OsuSetting.MenuParallax, true);
|
||||||
@ -89,6 +90,7 @@ namespace osu.Game.Configuration
|
|||||||
GameplayCursorSize,
|
GameplayCursorSize,
|
||||||
AutoCursorSize,
|
AutoCursorSize,
|
||||||
DimLevel,
|
DimLevel,
|
||||||
|
ShowStoryboard,
|
||||||
KeyOverlay,
|
KeyOverlay,
|
||||||
FloatingComments,
|
FloatingComments,
|
||||||
PlaybackSpeed,
|
PlaybackSpeed,
|
||||||
|
@ -14,6 +14,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
{
|
{
|
||||||
Children = new[]
|
Children = new[]
|
||||||
{
|
{
|
||||||
|
new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = "Storyboards",
|
||||||
|
Bindable = config.GetBindable<bool>(OsuSetting.ShowStoryboard)
|
||||||
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = "Rotate cursor when dragging",
|
LabelText = "Rotate cursor when dragging",
|
||||||
|
@ -24,6 +24,8 @@ using osu.Game.Screens.Ranking;
|
|||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Storyboards.Drawables;
|
||||||
|
using OpenTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Play
|
namespace osu.Game.Screens.Play
|
||||||
{
|
{
|
||||||
@ -59,6 +61,7 @@ namespace osu.Game.Screens.Play
|
|||||||
#region User Settings
|
#region User Settings
|
||||||
|
|
||||||
private Bindable<double> dimLevel;
|
private Bindable<double> dimLevel;
|
||||||
|
private Bindable<bool> showStoryboard;
|
||||||
private Bindable<bool> mouseWheelDisabled;
|
private Bindable<bool> mouseWheelDisabled;
|
||||||
private Bindable<double> userAudioOffset;
|
private Bindable<double> userAudioOffset;
|
||||||
|
|
||||||
@ -66,6 +69,9 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
private Container storyboardContainer;
|
||||||
|
private DrawableStoryboard storyboard;
|
||||||
|
|
||||||
private HUDOverlay hudOverlay;
|
private HUDOverlay hudOverlay;
|
||||||
private FailOverlay failOverlay;
|
private FailOverlay failOverlay;
|
||||||
|
|
||||||
@ -77,6 +83,7 @@ namespace osu.Game.Screens.Play
|
|||||||
this.api = api;
|
this.api = api;
|
||||||
|
|
||||||
dimLevel = config.GetBindable<double>(OsuSetting.DimLevel);
|
dimLevel = config.GetBindable<double>(OsuSetting.DimLevel);
|
||||||
|
showStoryboard = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
|
||||||
|
|
||||||
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
|
mouseWheelDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableWheel);
|
||||||
|
|
||||||
@ -145,6 +152,12 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
storyboardContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Clock = offsetClock,
|
||||||
|
Alpha = 0,
|
||||||
|
},
|
||||||
pauseContainer = new PauseContainer
|
pauseContainer = new PauseContainer
|
||||||
{
|
{
|
||||||
AudioClock = decoupledClock,
|
AudioClock = decoupledClock,
|
||||||
@ -196,6 +209,9 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
scoreProcessor = RulesetContainer.CreateScoreProcessor();
|
scoreProcessor = RulesetContainer.CreateScoreProcessor();
|
||||||
|
|
||||||
|
if (showStoryboard)
|
||||||
|
initializeStoryboard(false);
|
||||||
|
|
||||||
hudOverlay.BindProcessor(scoreProcessor);
|
hudOverlay.BindProcessor(scoreProcessor);
|
||||||
hudOverlay.BindRulesetContainer(RulesetContainer);
|
hudOverlay.BindRulesetContainer(RulesetContainer);
|
||||||
|
|
||||||
@ -211,6 +227,16 @@ namespace osu.Game.Screens.Play
|
|||||||
scoreProcessor.Failed += onFail;
|
scoreProcessor.Failed += onFail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeStoryboard(bool asyncLoad)
|
||||||
|
{
|
||||||
|
var beatmap = Beatmap.Value.Beatmap;
|
||||||
|
|
||||||
|
storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value);
|
||||||
|
storyboard.Masking = true;
|
||||||
|
|
||||||
|
storyboardContainer.Add(asyncLoad ? new AsyncLoadWrapper(storyboard) { RelativeSizeAxes = Axes.Both } : (Drawable)storyboard);
|
||||||
|
}
|
||||||
|
|
||||||
public void Restart()
|
public void Restart()
|
||||||
{
|
{
|
||||||
sampleRestart?.Play();
|
sampleRestart?.Play();
|
||||||
@ -266,12 +292,12 @@ namespace osu.Game.Screens.Play
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
(Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, Easing.OutQuint);
|
(Background as BackgroundScreenBeatmap)?.BlurTo(Vector2.Zero, 1500, Easing.OutQuint);
|
||||||
Background?.FadeTo(1 - (float)dimLevel, 1500, Easing.OutQuint);
|
|
||||||
|
dimLevel.ValueChanged += dimLevel_ValueChanged;
|
||||||
|
showStoryboard.ValueChanged += showStoryboard_ValueChanged;
|
||||||
|
updateBackgroundElements();
|
||||||
|
|
||||||
Content.Alpha = 0;
|
Content.Alpha = 0;
|
||||||
|
|
||||||
dimLevel.ValueChanged += newDim => Background?.FadeTo(1 - (float)newDim, 800);
|
|
||||||
|
|
||||||
Content
|
Content
|
||||||
.ScaleTo(0.7f)
|
.ScaleTo(0.7f)
|
||||||
.ScaleTo(1, 750, Easing.OutQuint)
|
.ScaleTo(1, 750, Easing.OutQuint)
|
||||||
@ -310,8 +336,33 @@ namespace osu.Game.Screens.Play
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void dimLevel_ValueChanged(double newValue)
|
||||||
|
=> updateBackgroundElements();
|
||||||
|
|
||||||
|
private void showStoryboard_ValueChanged(bool newValue)
|
||||||
|
=> updateBackgroundElements();
|
||||||
|
|
||||||
|
private void updateBackgroundElements()
|
||||||
|
{
|
||||||
|
var opacity = 1 - (float)dimLevel;
|
||||||
|
|
||||||
|
if (showStoryboard && storyboard == null)
|
||||||
|
initializeStoryboard(true);
|
||||||
|
|
||||||
|
var beatmap = Beatmap.Value;
|
||||||
|
var storyboardVisible = showStoryboard && beatmap.Beatmap.Storyboard.HasDrawable;
|
||||||
|
|
||||||
|
storyboardContainer.FadeColour(new Color4(opacity, opacity, opacity, 1), 800);
|
||||||
|
storyboardContainer.FadeTo(storyboardVisible && opacity > 0 ? 1 : 0);
|
||||||
|
|
||||||
|
Background?.FadeTo(!storyboardVisible || beatmap.Background == null ? opacity : 0, 800, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
private void fadeOut()
|
private void fadeOut()
|
||||||
{
|
{
|
||||||
|
dimLevel.ValueChanged -= dimLevel_ValueChanged;
|
||||||
|
showStoryboard.ValueChanged -= showStoryboard_ValueChanged;
|
||||||
|
|
||||||
const float fade_out_duration = 250;
|
const float fade_out_duration = 250;
|
||||||
|
|
||||||
RulesetContainer?.FadeOut(fade_out_duration);
|
RulesetContainer?.FadeOut(fade_out_duration);
|
||||||
|
@ -10,8 +10,8 @@ namespace osu.Game.Storyboards
|
|||||||
public double LoopStartTime;
|
public double LoopStartTime;
|
||||||
public int LoopCount;
|
public int LoopCount;
|
||||||
|
|
||||||
public override double StartTime => LoopStartTime;
|
public override double StartTime => LoopStartTime + CommandsStartTime;
|
||||||
public override double EndTime => LoopStartTime + CommandsDuration * LoopCount;
|
public override double EndTime => StartTime + CommandsDuration * LoopCount;
|
||||||
|
|
||||||
public CommandLoop(double startTime, int loopCount)
|
public CommandLoop(double startTime, int loopCount)
|
||||||
{
|
{
|
||||||
|
@ -5,6 +5,7 @@ using OpenTK;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Game.IO;
|
using osu.Game.IO;
|
||||||
|
|
||||||
@ -14,6 +15,16 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
{
|
{
|
||||||
public Storyboard Storyboard { get; private set; }
|
public Storyboard Storyboard { get; private set; }
|
||||||
|
|
||||||
|
private readonly Background background;
|
||||||
|
public Texture BackgroundTexture
|
||||||
|
{
|
||||||
|
get { return background.Texture; }
|
||||||
|
set { background.Texture = value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Container<DrawableStoryboardLayer> content;
|
||||||
|
protected override Container<DrawableStoryboardLayer> Content => content;
|
||||||
|
|
||||||
protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480);
|
protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480);
|
||||||
public override bool HandleInput => false;
|
public override bool HandleInput => false;
|
||||||
|
|
||||||
@ -39,6 +50,18 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
Size = new Vector2(640, 480);
|
Size = new Vector2(640, 480);
|
||||||
Anchor = Anchor.Centre;
|
Anchor = Anchor.Centre;
|
||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
|
|
||||||
|
AddInternal(background = new Background
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
});
|
||||||
|
AddInternal(content = new Container<DrawableStoryboardLayer>
|
||||||
|
{
|
||||||
|
Size = new Vector2(640, 480),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -55,5 +78,10 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
foreach (var layer in Children)
|
foreach (var layer in Children)
|
||||||
layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing;
|
layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class Background : Sprite
|
||||||
|
{
|
||||||
|
protected override Vector2 DrawScale => Texture != null ? new Vector2(Parent.DrawHeight / Texture.DisplayHeight) : base.DrawScale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,6 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
{
|
{
|
||||||
public StoryboardAnimation Animation { get; private set; }
|
public StoryboardAnimation Animation { get; private set; }
|
||||||
|
|
||||||
protected override bool ShouldBeAlive => Animation.HasCommands && base.ShouldBeAlive;
|
|
||||||
public override bool RemoveWhenNotAlive => !Animation.HasCommands || base.RemoveWhenNotAlive;
|
|
||||||
|
|
||||||
public bool FlipH { get; set; }
|
public bool FlipH { get; set; }
|
||||||
public bool FlipV { get; set; }
|
public bool FlipV { get; set; }
|
||||||
|
|
||||||
@ -59,11 +56,8 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
Position = animation.InitialPosition;
|
Position = animation.InitialPosition;
|
||||||
Repeat = animation.LoopType == AnimationLoopType.LoopForever;
|
Repeat = animation.LoopType == AnimationLoopType.LoopForever;
|
||||||
|
|
||||||
if (animation.HasCommands)
|
LifetimeStart = animation.StartTime;
|
||||||
{
|
LifetimeEnd = animation.EndTime;
|
||||||
LifetimeStart = animation.StartTime;
|
|
||||||
LifetimeEnd = animation.EndTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -28,9 +28,8 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
{
|
{
|
||||||
foreach (var element in Layer.Elements)
|
foreach (var element in Layer.Elements)
|
||||||
{
|
{
|
||||||
var drawable = element.CreateDrawable();
|
if (element.IsDrawable)
|
||||||
if (drawable != null)
|
Add(element.CreateDrawable());
|
||||||
Add(drawable);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,6 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
{
|
{
|
||||||
public StoryboardSprite Sprite { get; private set; }
|
public StoryboardSprite Sprite { get; private set; }
|
||||||
|
|
||||||
protected override bool ShouldBeAlive => Sprite.HasCommands && base.ShouldBeAlive;
|
|
||||||
public override bool RemoveWhenNotAlive => !Sprite.HasCommands || base.RemoveWhenNotAlive;
|
|
||||||
|
|
||||||
public bool FlipH { get; set; }
|
public bool FlipH { get; set; }
|
||||||
public bool FlipV { get; set; }
|
public bool FlipV { get; set; }
|
||||||
|
|
||||||
@ -58,11 +55,8 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
Origin = sprite.Origin;
|
Origin = sprite.Origin;
|
||||||
Position = sprite.InitialPosition;
|
Position = sprite.InitialPosition;
|
||||||
|
|
||||||
if (sprite.HasCommands)
|
LifetimeStart = sprite.StartTime;
|
||||||
{
|
LifetimeEnd = sprite.EndTime;
|
||||||
LifetimeStart = sprite.StartTime;
|
|
||||||
LifetimeEnd = sprite.EndTime;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -8,6 +8,8 @@ namespace osu.Game.Storyboards
|
|||||||
public interface IStoryboardElement
|
public interface IStoryboardElement
|
||||||
{
|
{
|
||||||
string Path { get; }
|
string Path { get; }
|
||||||
|
bool IsDrawable { get; }
|
||||||
|
|
||||||
Drawable CreateDrawable();
|
Drawable CreateDrawable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Storyboards.Drawables;
|
using osu.Game.Storyboards.Drawables;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -12,6 +13,8 @@ namespace osu.Game.Storyboards
|
|||||||
private readonly Dictionary<string, StoryboardLayer> layers = new Dictionary<string, StoryboardLayer>();
|
private readonly Dictionary<string, StoryboardLayer> layers = new Dictionary<string, StoryboardLayer>();
|
||||||
public IEnumerable<StoryboardLayer> Layers => layers.Values;
|
public IEnumerable<StoryboardLayer> Layers => layers.Values;
|
||||||
|
|
||||||
|
public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable));
|
||||||
|
|
||||||
public Storyboard()
|
public Storyboard()
|
||||||
{
|
{
|
||||||
layers.Add("Background", new StoryboardLayer("Background", 3));
|
layers.Add("Background", new StoryboardLayer("Background", 3));
|
||||||
@ -29,7 +32,32 @@ namespace osu.Game.Storyboards
|
|||||||
return layer;
|
return layer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DrawableStoryboard CreateDrawable()
|
/// <summary>
|
||||||
=> new DrawableStoryboard(this);
|
/// Whether the beatmap's background should be hidden while this storyboard is being displayed.
|
||||||
|
/// </summary>
|
||||||
|
public bool ReplacesBackground(BeatmapInfo beatmapInfo)
|
||||||
|
{
|
||||||
|
var backgroundPath = beatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant();
|
||||||
|
if (backgroundPath == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float AspectRatio(BeatmapInfo beatmapInfo)
|
||||||
|
=> beatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f;
|
||||||
|
|
||||||
|
public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null)
|
||||||
|
{
|
||||||
|
var drawable = new DrawableStoryboard(this);
|
||||||
|
if (working != null)
|
||||||
|
{
|
||||||
|
var beatmapInfo = working.Beatmap.BeatmapInfo;
|
||||||
|
drawable.Width = drawable.Height * AspectRatio(beatmapInfo);
|
||||||
|
if (!ReplacesBackground(beatmapInfo))
|
||||||
|
drawable.BackgroundTexture = working.Background;
|
||||||
|
}
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,15 @@
|
|||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using System;
|
||||||
|
|
||||||
namespace osu.Game.Storyboards
|
namespace osu.Game.Storyboards
|
||||||
{
|
{
|
||||||
public class StoryboardSample : IStoryboardElement
|
public class StoryboardSample : IStoryboardElement
|
||||||
{
|
{
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
public bool IsDrawable => false;
|
||||||
|
|
||||||
public double Time;
|
public double Time;
|
||||||
public float Volume;
|
public float Volume;
|
||||||
|
|
||||||
@ -19,6 +22,8 @@ namespace osu.Game.Storyboards
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Drawable CreateDrawable()
|
public Drawable CreateDrawable()
|
||||||
=> null;
|
{
|
||||||
|
throw new InvalidOperationException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,8 @@ namespace osu.Game.Storyboards
|
|||||||
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
|
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
|
||||||
|
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
public bool IsDrawable => HasCommands;
|
||||||
|
|
||||||
public Anchor Origin;
|
public Anchor Origin;
|
||||||
public Vector2 InitialPosition;
|
public Vector2 InitialPosition;
|
||||||
|
|
||||||
|
@ -79,11 +79,13 @@ namespace osu.Game.Tests.Visual
|
|||||||
storyboardContainer.Remove(storyboard);
|
storyboardContainer.Remove(storyboard);
|
||||||
|
|
||||||
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
|
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
|
||||||
decoupledClock.ChangeSource(working.Track);
|
|
||||||
storyboardContainer.Clock = decoupledClock;
|
storyboardContainer.Clock = decoupledClock;
|
||||||
|
|
||||||
storyboardContainer.Add(storyboard = working.Beatmap.Storyboard.CreateDrawable());
|
storyboard = working.Beatmap.Storyboard.CreateDrawable(beatmapBacking);
|
||||||
storyboard.Passing = false;
|
storyboard.Passing = false;
|
||||||
|
|
||||||
|
storyboardContainer.Add(storyboard);
|
||||||
|
decoupledClock.ChangeSource(working.Track);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user