mirror of
https://github.com/osukey/osukey.git
synced 2025-05-29 09:27:18 +09:00
Merge pull request #1733 from smoogipoo/fix-taiko-stronghit
Yet another rework of sound handling in taiko
This commit is contained in:
commit
2fb619ed2d
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
StartTime = lastTickTime,
|
StartTime = lastTickTime,
|
||||||
ComboColour = ComboColour,
|
ComboColour = ComboColour,
|
||||||
X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
|
X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH,
|
||||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
|
||||||
{
|
{
|
||||||
Bank = s.Bank,
|
Bank = s.Bank,
|
||||||
Name = @"slidertick",
|
Name = @"slidertick",
|
||||||
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
StartTime = repeatStartTime + t,
|
StartTime = repeatStartTime + t,
|
||||||
ComboColour = ComboColour,
|
ComboColour = ComboColour,
|
||||||
X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
|
X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH,
|
||||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
|
||||||
{
|
{
|
||||||
Bank = s.Bank,
|
Bank = s.Bank,
|
||||||
Name = @"slidertick",
|
Name = @"slidertick",
|
||||||
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
set { Curve.ControlPoints = value; }
|
set { Curve.ControlPoints = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
|
public List<List<SampleInfo>> RepeatSamples { get; set; } = new List<List<SampleInfo>>();
|
||||||
|
|
||||||
public CurveType CurveType
|
public CurveType CurveType
|
||||||
{
|
{
|
||||||
|
@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private SampleInfoList sampleInfoListAt(double time)
|
private List<SampleInfo> sampleInfoListAt(double time)
|
||||||
{
|
{
|
||||||
var curveData = HitObject as IHasCurve;
|
var curveData = HitObject as IHasCurve;
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// 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 System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -435,7 +436,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the sample info list from.</param>
|
/// <param name="time">The time to retrieve the sample info list from.</param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
private SampleInfoList sampleInfoListAt(double time)
|
private List<SampleInfo> sampleInfoListAt(double time)
|
||||||
{
|
{
|
||||||
var curveData = HitObject as IHasCurve;
|
var curveData = HitObject as IHasCurve;
|
||||||
|
|
||||||
|
@ -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 System.Collections.Generic;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.MathUtils;
|
using osu.Game.Rulesets.Mania.MathUtils;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (hold.Head.Samples == null)
|
if (hold.Head.Samples == null)
|
||||||
hold.Head.Samples = new SampleInfoList();
|
hold.Head.Samples = new List<SampleInfo>();
|
||||||
|
|
||||||
hold.Head.Samples.Add(new SampleInfo { Name = SampleInfo.HIT_NORMAL });
|
hold.Head.Samples.Add(new SampleInfo { Name = SampleInfo.HIT_NORMAL });
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal float LazyTravelDistance;
|
internal float LazyTravelDistance;
|
||||||
|
|
||||||
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
|
public List<List<SampleInfo>> RepeatSamples { get; set; } = new List<List<SampleInfo>>();
|
||||||
public int RepeatCount { get; set; } = 1;
|
public int RepeatCount { get; set; } = 1;
|
||||||
|
|
||||||
private int stackHeight;
|
private int stackHeight;
|
||||||
@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
ComboColour = ComboColour,
|
ComboColour = ComboColour,
|
||||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
|
||||||
{
|
{
|
||||||
Bank = s.Bank,
|
Bank = s.Bank,
|
||||||
Name = @"slidertick",
|
Name = @"slidertick",
|
||||||
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
StackHeight = StackHeight,
|
StackHeight = StackHeight,
|
||||||
Scale = Scale,
|
Scale = Scale,
|
||||||
ComboColour = ComboColour,
|
ComboColour = ComboColour,
|
||||||
Samples = new SampleInfoList(RepeatSamples[repeat])
|
Samples = new List<SampleInfo>(RepeatSamples[repeat])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
47
osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
Normal file
47
osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
using osu.Framework.Audio.Sample;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Audio
|
||||||
|
{
|
||||||
|
public class DrumSampleMapping
|
||||||
|
{
|
||||||
|
private readonly ControlPointInfo controlPoints;
|
||||||
|
private readonly Dictionary<SampleControlPoint, DrumSample> mappings = new Dictionary<SampleControlPoint, DrumSample>();
|
||||||
|
|
||||||
|
public DrumSampleMapping(ControlPointInfo controlPoints, AudioManager audio)
|
||||||
|
{
|
||||||
|
this.controlPoints = controlPoints;
|
||||||
|
|
||||||
|
IEnumerable<SampleControlPoint> samplePoints;
|
||||||
|
if (controlPoints.SamplePoints.Count == 0)
|
||||||
|
// Get the default sample point
|
||||||
|
samplePoints = new[] { controlPoints.SamplePointAt(double.MinValue) };
|
||||||
|
else
|
||||||
|
samplePoints = controlPoints.SamplePoints;
|
||||||
|
|
||||||
|
foreach (var s in samplePoints.Distinct())
|
||||||
|
{
|
||||||
|
mappings[s] = new DrumSample
|
||||||
|
{
|
||||||
|
Centre = s.GetSampleInfo().GetChannel(audio.Sample),
|
||||||
|
Rim = s.GetSampleInfo(SampleInfo.HIT_CLAP).GetChannel(audio.Sample)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time)];
|
||||||
|
|
||||||
|
public class DrumSample
|
||||||
|
{
|
||||||
|
public SampleChannel Centre;
|
||||||
|
public SampleChannel Rim;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
var curveData = obj as IHasCurve;
|
var curveData = obj as IHasCurve;
|
||||||
|
|
||||||
// Old osu! used hit sounding to determine various hit type information
|
// Old osu! used hit sounding to determine various hit type information
|
||||||
SampleInfoList samples = obj.Samples;
|
List<SampleInfo> samples = obj.Samples;
|
||||||
|
|
||||||
bool strong = samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
bool strong = samples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
||||||
|
|
||||||
@ -115,12 +115,12 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
|
|||||||
|
|
||||||
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
|
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
|
||||||
{
|
{
|
||||||
List<SampleInfoList> allSamples = curveData != null ? curveData.RepeatSamples : new List<SampleInfoList>(new[] { samples });
|
List<List<SampleInfo>> allSamples = curveData != null ? curveData.RepeatSamples : new List<List<SampleInfo>>(new[] { samples });
|
||||||
|
|
||||||
int i = 0;
|
int i = 0;
|
||||||
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
|
for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing)
|
||||||
{
|
{
|
||||||
SampleInfoList currentSamples = allSamples[i];
|
List<SampleInfo> currentSamples = allSamples[i];
|
||||||
bool isRim = currentSamples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE);
|
bool isRim = currentSamples.Any(s => s.Name == SampleInfo.HIT_CLAP || s.Name == SampleInfo.HIT_WHISTLE);
|
||||||
strong = currentSamples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
strong = currentSamples.Any(s => s.Name == SampleInfo.HIT_FINISH);
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
validKeyPressed = HitActions.Contains(action);
|
validKeyPressed = HitActions.Contains(action);
|
||||||
|
|
||||||
// Only count this as handled if the new judgement is a hit
|
// Only count this as handled if the new judgement is a hit
|
||||||
return UpdateJudgement(true) && Judgements.LastOrDefault()?.IsHit == true;
|
return UpdateJudgement(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
if (!userTriggered)
|
if (!userTriggered)
|
||||||
{
|
{
|
||||||
if (timeOffset > second_hit_window)
|
if (timeOffset > second_hit_window)
|
||||||
AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.Miss });
|
AddJudgement(new TaikoStrongHitJudgement { Result = HitResult.None });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Assume the intention was to hit the strong hit with both keys only if the first key is still being held down
|
// Assume the intention was to hit the strong hit with both keys only if the first key is still being held down
|
||||||
return firstKeyHeld && UpdateJudgement(true) && Judgements.LastOrDefault()?.IsHit == true;
|
return firstKeyHeld && UpdateJudgement(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,10 +34,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
private readonly CircularContainer targetRing;
|
private readonly CircularContainer targetRing;
|
||||||
private readonly CircularContainer expandingRing;
|
private readonly CircularContainer expandingRing;
|
||||||
|
|
||||||
private readonly TaikoAction[] rimActions = { TaikoAction.LeftRim, TaikoAction.RightRim };
|
|
||||||
private readonly TaikoAction[] centreActions = { TaikoAction.LeftCentre, TaikoAction.RightCentre };
|
|
||||||
private TaikoAction[] lastAction;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The amount of times the user has hit this swell.
|
/// The amount of times the user has hit this swell.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -205,19 +201,20 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool? lastWasCentre;
|
||||||
|
|
||||||
public override bool OnPressed(TaikoAction action)
|
public override bool OnPressed(TaikoAction action)
|
||||||
{
|
{
|
||||||
// Don't handle keys before the swell starts
|
// Don't handle keys before the swell starts
|
||||||
if (Time.Current < HitObject.StartTime)
|
if (Time.Current < HitObject.StartTime)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Find the keyset which this key corresponds to
|
var isCentre = action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre;
|
||||||
var keySet = rimActions.Contains(action) ? rimActions : centreActions;
|
|
||||||
|
|
||||||
// Ensure alternating keysets
|
// Ensure alternating centre and rim hits
|
||||||
if (keySet == lastAction)
|
if (lastWasCentre == isCentre)
|
||||||
return false;
|
return false;
|
||||||
lastAction = keySet;
|
lastWasCentre = isCentre;
|
||||||
|
|
||||||
UpdateJudgement(true);
|
UpdateJudgement(true);
|
||||||
|
|
||||||
|
@ -6,6 +6,9 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces;
|
||||||
using OpenTK;
|
using OpenTK;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||||
{
|
{
|
||||||
@ -35,6 +38,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
|||||||
MainPiece.KiaiMode = HitObject.Kiai;
|
MainPiece.KiaiMode = HitObject.Kiai;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Normal and clap samples are handled by the drum
|
||||||
|
protected override IEnumerable<SampleInfo> GetSamples() => HitObject.Samples.Where(s => s.Name != SampleInfo.HIT_NORMAL && s.Name != SampleInfo.HIT_CLAP);
|
||||||
|
|
||||||
protected virtual TaikoPiece CreateMainPiece() => new CirclePiece();
|
protected virtual TaikoPiece CreateMainPiece() => new CirclePiece();
|
||||||
|
|
||||||
public abstract bool OnPressed(TaikoAction action);
|
public abstract bool OnPressed(TaikoAction action);
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -75,7 +76,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
TickSpacing = tickSpacing,
|
TickSpacing = tickSpacing,
|
||||||
StartTime = t,
|
StartTime = t,
|
||||||
IsStrong = IsStrong,
|
IsStrong = IsStrong,
|
||||||
Samples = new SampleInfoList(Samples.Select(s => new SampleInfo
|
Samples = new List<SampleInfo>(Samples.Select(s => new SampleInfo
|
||||||
{
|
{
|
||||||
Bank = s.Bank,
|
Bank = s.Bank,
|
||||||
Name = @"slidertick",
|
Name = @"slidertick",
|
||||||
|
@ -16,4 +16,4 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public int RequiredHits = 10;
|
public int RequiredHits = 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -165,11 +165,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
|
|
||||||
private void addSwell(double duration = default_duration)
|
private void addSwell(double duration = default_duration)
|
||||||
{
|
{
|
||||||
rulesetContainer.Playfield.Add(new DrawableSwell(new Swell
|
var swell = new Swell
|
||||||
{
|
{
|
||||||
StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
|
StartTime = rulesetContainer.Playfield.Time.Current + scroll_time,
|
||||||
Duration = duration,
|
Duration = duration,
|
||||||
}));
|
};
|
||||||
|
|
||||||
|
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
|
rulesetContainer.Playfield.Add(new DrawableSwell(swell));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addDrumRoll(bool strong, double duration = default_duration)
|
private void addDrumRoll(bool strong, double duration = default_duration)
|
||||||
@ -184,6 +188,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
Duration = duration,
|
Duration = duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
d.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
rulesetContainer.Playfield.Add(new DrawableDrumRoll(d));
|
rulesetContainer.Playfield.Add(new DrawableDrumRoll(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,6 +201,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
IsStrong = strong
|
IsStrong = strong
|
||||||
};
|
};
|
||||||
|
|
||||||
|
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
if (strong)
|
if (strong)
|
||||||
rulesetContainer.Playfield.Add(new DrawableCentreHitStrong(h));
|
rulesetContainer.Playfield.Add(new DrawableCentreHitStrong(h));
|
||||||
else
|
else
|
||||||
@ -209,6 +217,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
IsStrong = strong
|
IsStrong = strong
|
||||||
};
|
};
|
||||||
|
|
||||||
|
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||||
|
|
||||||
if (strong)
|
if (strong)
|
||||||
rulesetContainer.Playfield.Add(new DrawableRimHitStrong(h));
|
rulesetContainer.Playfield.Add(new DrawableRimHitStrong(h));
|
||||||
else
|
else
|
||||||
|
@ -4,12 +4,15 @@
|
|||||||
using System;
|
using System;
|
||||||
using OpenTK;
|
using OpenTK;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
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.Sprites;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Rulesets.Taiko.Audio;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.UI
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
{
|
{
|
||||||
@ -18,16 +21,26 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal class InputDrum : Container
|
internal class InputDrum : Container
|
||||||
{
|
{
|
||||||
public InputDrum()
|
private const float middle_split = 0.025f;
|
||||||
|
|
||||||
|
private readonly ControlPointInfo controlPoints;
|
||||||
|
|
||||||
|
public InputDrum(ControlPointInfo controlPoints)
|
||||||
{
|
{
|
||||||
|
this.controlPoints = controlPoints;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
FillMode = FillMode.Fit;
|
FillMode = FillMode.Fit;
|
||||||
|
}
|
||||||
|
|
||||||
const float middle_split = 0.025f;
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(AudioManager audio)
|
||||||
|
{
|
||||||
|
var sampleMappings = new DrumSampleMapping(controlPoints, audio);
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new TaikoHalfDrum(false)
|
new TaikoHalfDrum(false, sampleMappings)
|
||||||
{
|
{
|
||||||
Name = "Left Half",
|
Name = "Left Half",
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
@ -38,7 +51,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
RimAction = TaikoAction.LeftRim,
|
RimAction = TaikoAction.LeftRim,
|
||||||
CentreAction = TaikoAction.LeftCentre
|
CentreAction = TaikoAction.LeftCentre
|
||||||
},
|
},
|
||||||
new TaikoHalfDrum(true)
|
new TaikoHalfDrum(true, sampleMappings)
|
||||||
{
|
{
|
||||||
Name = "Right Half",
|
Name = "Right Half",
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
@ -72,8 +85,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
private readonly Sprite centre;
|
private readonly Sprite centre;
|
||||||
private readonly Sprite centreHit;
|
private readonly Sprite centreHit;
|
||||||
|
|
||||||
public TaikoHalfDrum(bool flipped)
|
private readonly DrumSampleMapping sampleMappings;
|
||||||
|
|
||||||
|
public TaikoHalfDrum(bool flipped, DrumSampleMapping sampleMappings)
|
||||||
{
|
{
|
||||||
|
this.sampleMappings = sampleMappings;
|
||||||
|
|
||||||
Masking = true;
|
Masking = true;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
@ -128,15 +145,21 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
Drawable target = null;
|
Drawable target = null;
|
||||||
Drawable back = null;
|
Drawable back = null;
|
||||||
|
|
||||||
|
var drumSample = sampleMappings.SampleAt(Time.Current);
|
||||||
|
|
||||||
if (action == CentreAction)
|
if (action == CentreAction)
|
||||||
{
|
{
|
||||||
target = centreHit;
|
target = centreHit;
|
||||||
back = centre;
|
back = centre;
|
||||||
|
|
||||||
|
drumSample.Centre.Play();
|
||||||
}
|
}
|
||||||
else if (action == RimAction)
|
else if (action == RimAction)
|
||||||
{
|
{
|
||||||
target = rimHit;
|
target = rimHit;
|
||||||
back = rim;
|
back = rim;
|
||||||
|
|
||||||
|
drumSample.Rim.Play();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target != null)
|
if (target != null)
|
||||||
|
@ -16,17 +16,11 @@ using osu.Framework.Extensions.Color4Extensions;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
using osu.Game.Rulesets.Taiko.Objects.Drawables;
|
||||||
using osu.Framework.Input.Bindings;
|
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Framework.Audio;
|
|
||||||
using osu.Framework.Audio.Sample;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using osu.Game.Audio;
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.UI
|
namespace osu.Game.Rulesets.Taiko.UI
|
||||||
{
|
{
|
||||||
public class TaikoPlayfield : ScrollingPlayfield, IKeyBindingHandler<TaikoAction>
|
public class TaikoPlayfield : ScrollingPlayfield
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="TaikoRulesetContainer"/>.
|
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="TaikoRulesetContainer"/>.
|
||||||
@ -61,13 +55,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
private readonly Box overlayBackground;
|
private readonly Box overlayBackground;
|
||||||
private readonly Box background;
|
private readonly Box background;
|
||||||
|
|
||||||
private readonly ControlPointInfo controlPointInfo;
|
public TaikoPlayfield(ControlPointInfo controlPoints)
|
||||||
private Dictionary<SampleControlPoint, DrumSamples> drumSampleMappings;
|
|
||||||
|
|
||||||
public TaikoPlayfield(ControlPointInfo controlPointInfo)
|
|
||||||
: base(Axes.X)
|
: base(Axes.X)
|
||||||
{
|
{
|
||||||
this.controlPointInfo = controlPointInfo;
|
|
||||||
AddRangeInternal(new Drawable[]
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
backgroundContainer = new Container
|
backgroundContainer = new Container
|
||||||
@ -160,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
new InputDrum
|
new InputDrum(controlPoints)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
@ -205,19 +195,8 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuColour colours, AudioManager audio)
|
private void load(OsuColour colours)
|
||||||
{
|
{
|
||||||
drumSampleMappings = new Dictionary<SampleControlPoint, DrumSamples>();
|
|
||||||
foreach (var s in controlPointInfo.SamplePoints)
|
|
||||||
{
|
|
||||||
drumSampleMappings.Add(s,
|
|
||||||
new DrumSamples
|
|
||||||
{
|
|
||||||
Centre = s.GetSampleInfo().GetChannel(audio.Sample),
|
|
||||||
Rim = s.GetSampleInfo(SampleInfo.HIT_CLAP).GetChannel(audio.Sample)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
overlayBackgroundContainer.BorderColour = colours.Gray0;
|
overlayBackgroundContainer.BorderColour = colours.Gray0;
|
||||||
overlayBackground.Colour = colours.Gray1;
|
overlayBackground.Colour = colours.Gray1;
|
||||||
|
|
||||||
@ -282,28 +261,5 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim));
|
kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject, isRim));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(TaikoAction action)
|
|
||||||
{
|
|
||||||
var samplePoint = controlPointInfo.SamplePointAt(Clock.CurrentTime);
|
|
||||||
|
|
||||||
if (!drumSampleMappings.TryGetValue(samplePoint, out var samples))
|
|
||||||
throw new InvalidOperationException("Current sample set not found.");
|
|
||||||
|
|
||||||
if (action == TaikoAction.LeftCentre || action == TaikoAction.RightCentre)
|
|
||||||
samples.Centre.Play();
|
|
||||||
else
|
|
||||||
samples.Rim.Play();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool OnReleased(TaikoAction action) => false;
|
|
||||||
|
|
||||||
private class DrumSamples
|
|
||||||
{
|
|
||||||
public SampleChannel Centre;
|
|
||||||
public SampleChannel Rim;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,7 @@
|
|||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Include="Audio\DrumSampleMapping.cs" />
|
||||||
<Compile Include="Beatmaps\TaikoBeatmapConverter.cs" />
|
<Compile Include="Beatmaps\TaikoBeatmapConverter.cs" />
|
||||||
<Compile Include="Judgements\TaikoDrumRollTickJudgement.cs" />
|
<Compile Include="Judgements\TaikoDrumRollTickJudgement.cs" />
|
||||||
<Compile Include="Judgements\TaikoStrongHitJudgement.cs" />
|
<Compile Include="Judgements\TaikoStrongHitJudgement.cs" />
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace osu.Game.Audio
|
|
||||||
{
|
|
||||||
public class SampleInfoList : List<SampleInfo>
|
|
||||||
{
|
|
||||||
public SampleInfoList()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public SampleInfoList(IEnumerable<SampleInfo> elements) : base(elements)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
public IReadOnlyList<Judgement> Judgements => judgements;
|
public IReadOnlyList<Judgement> Judgements => judgements;
|
||||||
|
|
||||||
protected List<SampleChannel> Samples = new List<SampleChannel>();
|
protected List<SampleChannel> Samples = new List<SampleChannel>();
|
||||||
|
protected virtual IEnumerable<SampleInfo> GetSamples() => HitObject.Samples;
|
||||||
|
|
||||||
public readonly Bindable<ArmedState> State = new Bindable<ArmedState>();
|
public readonly Bindable<ArmedState> State = new Bindable<ArmedState>();
|
||||||
|
|
||||||
@ -84,12 +85,14 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(AudioManager audio)
|
private void load(AudioManager audio)
|
||||||
{
|
{
|
||||||
if (HitObject.Samples != null)
|
var samples = GetSamples();
|
||||||
|
if (samples.Any())
|
||||||
{
|
{
|
||||||
if (HitObject.SampleControlPoint == null)
|
if (HitObject.SampleControlPoint == null)
|
||||||
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)} must always have an attached {nameof(HitObject.SampleControlPoint)}.");
|
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
|
||||||
|
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
|
||||||
|
|
||||||
foreach (SampleInfo s in HitObject.Samples)
|
foreach (SampleInfo s in samples)
|
||||||
{
|
{
|
||||||
SampleInfo localSampleInfo = new SampleInfo
|
SampleInfo localSampleInfo = new SampleInfo
|
||||||
{
|
{
|
||||||
@ -174,7 +177,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
|||||||
{
|
{
|
||||||
judgementOccurred = false;
|
judgementOccurred = false;
|
||||||
|
|
||||||
if (AllJudged || State != ArmedState.Idle)
|
if (AllJudged)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (NestedHitObjects != null)
|
if (NestedHitObjects != null)
|
||||||
|
@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual double StartTime { get; set; }
|
public virtual double StartTime { get; set; }
|
||||||
|
|
||||||
|
private List<SampleInfo> samples;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The samples to be played when this hit object is hit.
|
/// The samples to be played when this hit object is hit.
|
||||||
/// <para>
|
/// <para>
|
||||||
@ -32,7 +34,11 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
/// and can be treated as the default samples for the hit object.
|
/// and can be treated as the default samples for the hit object.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public SampleInfoList Samples;
|
public List<SampleInfo> Samples
|
||||||
|
{
|
||||||
|
get => samples ?? (samples = new List<SampleInfo>());
|
||||||
|
set => samples = value;
|
||||||
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public SampleControlPoint SampleControlPoint;
|
public SampleControlPoint SampleControlPoint;
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||||
{
|
{
|
||||||
return new ConvertSlider
|
return new ConvertSlider
|
||||||
{
|
{
|
||||||
|
@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate the final per-node samples
|
// Generate the final per-node samples
|
||||||
var nodeSamples = new List<SampleInfoList>(nodes);
|
var nodeSamples = new List<List<SampleInfo>>(nodes);
|
||||||
for (int i = 0; i <= repeatCount; i++)
|
for (int i = 0; i <= repeatCount; i++)
|
||||||
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
|
||||||
|
|
||||||
@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
/// <param name="repeatCount">The slider repeat count.</param>
|
/// <param name="repeatCount">The slider repeat count.</param>
|
||||||
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
|
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
|
||||||
/// <returns>The hit object.</returns>
|
/// <returns>The hit object.</returns>
|
||||||
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples);
|
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a legacy Spinner-type hit object.
|
/// Creates a legacy Spinner-type hit object.
|
||||||
@ -234,9 +234,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
/// <param name="endTime">The hold end time.</param>
|
/// <param name="endTime">The hold end time.</param>
|
||||||
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime);
|
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime);
|
||||||
|
|
||||||
private SampleInfoList convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
|
private List<SampleInfo> convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
|
||||||
{
|
{
|
||||||
var soundTypes = new SampleInfoList
|
var soundTypes = new List<SampleInfo>
|
||||||
{
|
{
|
||||||
new SampleInfo
|
new SampleInfo
|
||||||
{
|
{
|
||||||
|
@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
|||||||
|
|
||||||
public double Distance { get; set; }
|
public double Distance { get; set; }
|
||||||
|
|
||||||
public List<SampleInfoList> RepeatSamples { get; set; }
|
public List<List<SampleInfo>> RepeatSamples { get; set; }
|
||||||
public int RepeatCount { get; set; } = 1;
|
public int RepeatCount { get; set; } = 1;
|
||||||
|
|
||||||
public double EndTime => StartTime + RepeatCount * Distance / Velocity;
|
public double EndTime => StartTime + RepeatCount * Distance / Velocity;
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||||
{
|
{
|
||||||
return new ConvertSlider
|
return new ConvertSlider
|
||||||
{
|
{
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
// 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 OpenTK;
|
using OpenTK;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
||||||
{
|
{
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||||
{
|
{
|
||||||
return new ConvertSlider
|
return new ConvertSlider
|
||||||
{
|
{
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
// 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 OpenTK;
|
using OpenTK;
|
||||||
using osu.Game.Audio;
|
|
||||||
using osu.Game.Rulesets.Objects.Types;
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using osu.Game.Audio;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
||||||
{
|
{
|
||||||
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<SampleInfoList> repeatSamples)
|
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
|
||||||
{
|
{
|
||||||
return new ConvertSlider
|
return new ConvertSlider
|
||||||
{
|
{
|
||||||
|
@ -19,6 +19,6 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The samples to be played when each repeat node is hit (0 -> first repeat node, 1 -> second repeat node, etc).
|
/// The samples to be played when each repeat node is hit (0 -> first repeat node, 1 -> second repeat node, etc).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
List<SampleInfoList> RepeatSamples { get; }
|
List<List<SampleInfo>> RepeatSamples { get; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,7 +239,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Audio\SampleInfo.cs" />
|
<Compile Include="Audio\SampleInfo.cs" />
|
||||||
<Compile Include="Audio\SampleInfoList.cs" />
|
|
||||||
<Compile Include="Beatmaps\Beatmap.cs" />
|
<Compile Include="Beatmaps\Beatmap.cs" />
|
||||||
<Compile Include="Beatmaps\BeatmapConverter.cs" />
|
<Compile Include="Beatmaps\BeatmapConverter.cs" />
|
||||||
<Compile Include="Beatmaps\BeatmapDifficulty.cs" />
|
<Compile Include="Beatmaps\BeatmapDifficulty.cs" />
|
||||||
|
Loading…
x
Reference in New Issue
Block a user