mirror of
https://github.com/ppy/osu.git
synced 2025-01-07 00:42:54 +08:00
Merge branch 'master' into mania-column-background-skinning
This commit is contained in:
commit
fe35f696c3
@ -5,6 +5,6 @@
|
||||
"version": "3.1.100"
|
||||
},
|
||||
"msbuild-sdks": {
|
||||
"Microsoft.Build.Traversal": "2.0.24"
|
||||
"Microsoft.Build.Traversal": "2.0.32"
|
||||
}
|
||||
}
|
@ -52,6 +52,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.327.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.331.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
@ -72,10 +72,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap)
|
||||
{
|
||||
using (var catcher = new Catcher(beatmap.BeatmapInfo.BaseDifficulty))
|
||||
{
|
||||
halfCatcherWidth = catcher.CatchWidth * 0.5f;
|
||||
halfCatcherWidth *= 0.8f; // We're only using 80% of the catcher's width to simulate imperfect gameplay.
|
||||
}
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
|
@ -37,10 +37,15 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatcherAnimationState CurrentState { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable.
|
||||
/// </summary>
|
||||
private const float allowed_catch_range = 0.8f;
|
||||
|
||||
/// <summary>
|
||||
/// Width of the area that can be used to attempt catches during gameplay.
|
||||
/// </summary>
|
||||
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X);
|
||||
internal float CatchWidth => CatcherArea.CATCHER_SIZE * Math.Abs(Scale.X) * allowed_catch_range;
|
||||
|
||||
protected bool Dashing
|
||||
{
|
||||
|
41
osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
Normal file
41
osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs
Normal file
@ -0,0 +1,41 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="CompositeDrawable"/> which is placed somewhere within a <see cref="Column"/>.
|
||||
/// </summary>
|
||||
public class LegacyManiaColumnElement : CompositeDrawable
|
||||
{
|
||||
[Resolved(CanBeNull = true)]
|
||||
[CanBeNull]
|
||||
protected ManiaStage Stage { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
protected Column Column { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// The column index to use for texture lookups, in the case of no user-provided configuration.
|
||||
/// </summary>
|
||||
protected int FallbackColumnIndex { get; private set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
if (Stage == null)
|
||||
FallbackColumnIndex = Column.Index % 2 + 1;
|
||||
else
|
||||
{
|
||||
int dist = Math.Min(Column.Index, Stage.Columns.Count - Column.Index - 1);
|
||||
FallbackColumnIndex = dist % 2 + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
void handleHitCircle(DrawableHitCircle circle)
|
||||
{
|
||||
if (!circle.IsHovered)
|
||||
if (!circle.HitArea.IsHovered)
|
||||
return;
|
||||
|
||||
Debug.Assert(circle.HitObject.HitWindows != null);
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
/// <summary>
|
||||
/// A single follow point positioned between two adjacent <see cref="DrawableOsuHitObject"/>s.
|
||||
/// </summary>
|
||||
public class FollowPoint : Container
|
||||
public class FollowPoint : Container, IAnimationTimeReference
|
||||
{
|
||||
private const float width = 8;
|
||||
|
||||
@ -45,5 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
}
|
||||
}, confineMode: ConfineMode.NoScaling);
|
||||
}
|
||||
|
||||
public double AnimationStartTime { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +116,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
|
||||
int point = 0;
|
||||
|
||||
ClearInternal();
|
||||
|
||||
for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing)
|
||||
{
|
||||
float fraction = (float)d / distance;
|
||||
@ -126,13 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
|
||||
FollowPoint fp;
|
||||
|
||||
if (InternalChildren.Count > point)
|
||||
{
|
||||
fp = (FollowPoint)InternalChildren[point];
|
||||
fp.ClearTransforms();
|
||||
}
|
||||
else
|
||||
AddInternal(fp = new FollowPoint());
|
||||
AddInternal(fp = new FollowPoint());
|
||||
|
||||
fp.Position = pointStartPosition;
|
||||
fp.Rotation = rotation;
|
||||
@ -142,6 +138,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
if (firstTransformStartTime == null)
|
||||
firstTransformStartTime = fadeInTime;
|
||||
|
||||
fp.AnimationStartTime = fadeInTime;
|
||||
|
||||
using (fp.BeginAbsoluteSequence(fadeInTime))
|
||||
{
|
||||
fp.FadeIn(osuEnd.TimeFadeIn);
|
||||
|
@ -62,6 +62,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
}
|
||||
};
|
||||
|
||||
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
|
||||
|
||||
if (!overlayAboveNumber)
|
||||
ChangeInternalChildDepth(hitCircleText, -float.MaxValue);
|
||||
|
||||
state.BindTo(drawableObject.State);
|
||||
state.BindValueChanged(updateState, true);
|
||||
|
||||
|
@ -11,6 +11,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
SliderPathRadius,
|
||||
AllowSliderBallTint,
|
||||
CursorExpand,
|
||||
CursorRotate
|
||||
CursorRotate,
|
||||
HitCircleOverlayAboveNumber
|
||||
}
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
|
||||
|
||||
protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (archive != null)
|
||||
beatmapSet.Beatmaps = createBeatmapDifficulties(beatmapSet.Files);
|
||||
@ -103,7 +103,19 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
validateOnlineIds(beatmapSet);
|
||||
|
||||
return updateQueue.UpdateAsync(beatmapSet, cancellationToken);
|
||||
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
|
||||
|
||||
await updateQueue.UpdateAsync(beatmapSet, cancellationToken);
|
||||
|
||||
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
|
||||
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
|
||||
{
|
||||
if (beatmapSet.OnlineBeatmapSetID != null)
|
||||
{
|
||||
beatmapSet.OnlineBeatmapSetID = null;
|
||||
LogForModel(beatmapSet, "Disassociating beatmap set ID due to loss of all beatmap IDs");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PreImport(BeatmapSetInfo beatmapSet)
|
||||
@ -447,12 +459,15 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
var res = req.Result;
|
||||
|
||||
beatmap.Status = res.Status;
|
||||
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
|
||||
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
|
||||
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
|
||||
if (res != null)
|
||||
{
|
||||
beatmap.Status = res.Status;
|
||||
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
|
||||
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
|
||||
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
|
||||
|
||||
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
|
||||
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
@ -12,11 +12,11 @@ namespace osu.Game.Online.API
|
||||
/// An API request with a well-defined response type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of the response (used for deserialisation).</typeparam>
|
||||
public abstract class APIRequest<T> : APIRequest
|
||||
public abstract class APIRequest<T> : APIRequest where T : class
|
||||
{
|
||||
protected override WebRequest CreateWebRequest() => new OsuJsonWebRequest<T>(Uri);
|
||||
|
||||
public T Result => ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
|
||||
public T Result => ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
|
||||
|
||||
protected APIRequest()
|
||||
{
|
||||
|
@ -6,7 +6,7 @@ using osu.Game.Rulesets;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public abstract class GetRankingsRequest<TModel> : APIRequest<TModel>
|
||||
public abstract class GetRankingsRequest<TModel> : APIRequest<TModel> where TModel : class
|
||||
{
|
||||
private readonly RulesetInfo ruleset;
|
||||
private readonly int page;
|
||||
|
@ -6,7 +6,7 @@ using osu.Framework.IO.Network;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
{
|
||||
public abstract class PaginatedAPIRequest<T> : APIRequest<T>
|
||||
public abstract class PaginatedAPIRequest<T> : APIRequest<T> where T : class
|
||||
{
|
||||
private readonly int page;
|
||||
private readonly int itemsPerPage;
|
||||
|
@ -28,10 +28,11 @@ namespace osu.Game.Scoring.Legacy
|
||||
{
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = new ScoreInfo(),
|
||||
Replay = new Replay()
|
||||
};
|
||||
|
||||
WorkingBeatmap workingBeatmap;
|
||||
|
||||
using (SerializationReader sr = new SerializationReader(stream))
|
||||
{
|
||||
currentRuleset = GetRuleset(sr.ReadByte());
|
||||
@ -41,7 +42,7 @@ namespace osu.Game.Scoring.Legacy
|
||||
|
||||
var version = sr.ReadInt32();
|
||||
|
||||
var workingBeatmap = GetBeatmap(sr.ReadString());
|
||||
workingBeatmap = GetBeatmap(sr.ReadString());
|
||||
if (workingBeatmap is DummyWorkingBeatmap)
|
||||
throw new BeatmapNotFoundException();
|
||||
|
||||
@ -113,6 +114,10 @@ namespace osu.Game.Scoring.Legacy
|
||||
|
||||
CalculateAccuracy(score.ScoreInfo);
|
||||
|
||||
// before returning for database import, we must restore the database-sourced BeatmapInfo.
|
||||
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
|
||||
score.ScoreInfo.Beatmap = workingBeatmap.BeatmapInfo;
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
@ -29,6 +30,8 @@ namespace osu.Game.Screens.Play
|
||||
this.Push(CreateResults(DrawableRuleset.ReplayScore.ScoreInfo));
|
||||
}
|
||||
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score) => new ResultsScreen(score, false);
|
||||
|
||||
protected override ScoreInfo CreateScore() => score.ScoreInfo;
|
||||
}
|
||||
}
|
||||
|
@ -33,16 +33,21 @@ namespace osu.Game.Screens.Ranking
|
||||
|
||||
public readonly ScoreInfo Score;
|
||||
|
||||
private readonly bool allowRetry;
|
||||
|
||||
private Drawable bottomPanel;
|
||||
|
||||
public ResultsScreen(ScoreInfo score)
|
||||
public ResultsScreen(ScoreInfo score, bool allowRetry = true)
|
||||
{
|
||||
Score = score;
|
||||
this.allowRetry = allowRetry;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
FillFlowContainer buttons;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new ResultsScrollContainer
|
||||
@ -68,7 +73,7 @@ namespace osu.Game.Screens.Ranking
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex("#333")
|
||||
},
|
||||
new FillFlowContainer
|
||||
buttons = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -78,15 +83,16 @@ namespace osu.Game.Screens.Ranking
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new ReplayDownloadButton(Score) { Width = 300 },
|
||||
new RetryButton { Width = 300 },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (player != null)
|
||||
if (player != null && allowRetry)
|
||||
{
|
||||
buttons.Add(new RetryButton { Width = 300 });
|
||||
|
||||
AddInternal(new HotkeyRetryOverlay
|
||||
{
|
||||
Action = () =>
|
||||
|
25
osu.Game/Skinning/IAnimationTimeReference.cs
Normal file
25
osu.Game/Skinning/IAnimationTimeReference.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
/// <summary>
|
||||
/// Denotes an object which provides a reference time to start animations from.
|
||||
/// </summary>
|
||||
[Cached]
|
||||
public interface IAnimationTimeReference
|
||||
{
|
||||
/// <summary>
|
||||
/// The reference clock.
|
||||
/// </summary>
|
||||
IFrameBasedClock Clock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time which animations should be started from, relative to <see cref="Clock"/>.
|
||||
/// </summary>
|
||||
double AnimationStartTime { get; }
|
||||
}
|
||||
}
|
@ -3,10 +3,12 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Animations;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Timing;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@ -22,7 +24,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
if (textures.Length > 0)
|
||||
{
|
||||
var animation = new TextureAnimation
|
||||
var animation = new SkinnableTextureAnimation
|
||||
{
|
||||
DefaultFrameLength = getFrameLength(source, applyConfigFrameRate, textures),
|
||||
Repeat = looping,
|
||||
@ -53,6 +55,25 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
public class SkinnableTextureAnimation : TextureAnimation
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
private IAnimationTimeReference timeReference { get; set; }
|
||||
|
||||
public SkinnableTextureAnimation()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (timeReference != null)
|
||||
Clock = new FramedOffsetClock(timeReference.Clock) { Offset = -timeReference.AnimationStartTime };
|
||||
}
|
||||
}
|
||||
|
||||
private const double default_frame_time = 1000 / 60d;
|
||||
|
||||
private static double getFrameLength(ISkin source, bool applyConfigFrameRate, Texture[] textures)
|
||||
|
@ -50,7 +50,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
|
||||
AddInternal(Content = new Container<DrawableStoryboardLayer>
|
||||
{
|
||||
Size = new Vector2(640, 480),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Storyboards.Drawables
|
||||
{
|
||||
public class DrawableStoryboardLayer : LifetimeManagementContainer
|
||||
public class DrawableStoryboardLayer : CompositeDrawable
|
||||
{
|
||||
public StoryboardLayer Layer { get; }
|
||||
public bool Enabled;
|
||||
@ -23,17 +23,34 @@ namespace osu.Game.Storyboards.Drawables
|
||||
Origin = Anchor.Centre;
|
||||
Enabled = layer.VisibleWhenPassing;
|
||||
Masking = layer.Masking;
|
||||
|
||||
InternalChild = new LayerElementContainer(layer);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CancellationToken? cancellationToken)
|
||||
private class LayerElementContainer : LifetimeManagementContainer
|
||||
{
|
||||
foreach (var element in Layer.Elements)
|
||||
{
|
||||
cancellationToken?.ThrowIfCancellationRequested();
|
||||
private readonly StoryboardLayer storyboardLayer;
|
||||
|
||||
if (element.IsDrawable)
|
||||
AddInternal(element.CreateDrawable());
|
||||
public LayerElementContainer(StoryboardLayer layer)
|
||||
{
|
||||
storyboardLayer = layer;
|
||||
|
||||
Width = 640;
|
||||
Height = 480;
|
||||
Anchor = Anchor.Centre;
|
||||
Origin = Anchor.Centre;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CancellationToken? cancellationToken)
|
||||
{
|
||||
foreach (var element in storyboardLayer.Elements)
|
||||
{
|
||||
cancellationToken?.ThrowIfCancellationRequested();
|
||||
|
||||
if (element.IsDrawable)
|
||||
AddInternal(element.CreateDrawable());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.327.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.331.0" />
|
||||
<PackageReference Include="Sentry" Version="2.1.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
|
@ -71,7 +71,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.315.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.327.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.331.0" />
|
||||
</ItemGroup>
|
||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
@ -79,7 +79,7 @@
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.327.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2020.331.0" />
|
||||
<PackageReference Include="SharpCompress" Version="0.24.0" />
|
||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user