1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 23:47:24 +08:00

Merge branch 'master' into taiko_replays

This commit is contained in:
smoogipooo 2017-03-31 16:11:58 +09:00
commit fb6f27a439
27 changed files with 595 additions and 383 deletions

@ -1 +1 @@
Subproject commit 269a1fd192c573d558a5ab0ff990a8b462947287
Subproject commit 7ae3cf1a10fa973a49995a71cbaf768702be1cce

View File

@ -4,7 +4,6 @@
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Graphics;
@ -76,20 +75,16 @@ namespace osu.Desktop.VisualTests.Tests
}
});
Add(new SwellCircle(new CirclePiece
Add(new CirclePiece
{
KiaiMode = kiai
})
{
Position = new Vector2(100, 500)
});
Add(new SwellCircle(new StrongCirclePiece
{
KiaiMode = kiai
})
{
Position = new Vector2(350, 500)
Position = new Vector2(100, 500),
Width = 0,
AccentColour = Color4.Orange,
KiaiMode = kiai,
Children = new[]
{
new SwellSymbolPiece()
}
});
Add(new DrumRollCircle(new CirclePiece
@ -111,28 +106,6 @@ namespace osu.Desktop.VisualTests.Tests
});
}
private class SwellCircle : BaseCircle
{
public SwellCircle(CirclePiece piece)
: base(piece)
{
Piece.Add(new TextAwesome
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
TextSize = CirclePiece.SYMBOL_INNER_SIZE,
Icon = FontAwesome.fa_asterisk,
Shadow = false
});
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Piece.AccentColour = colours.YellowDark;
}
}
private class DrumRollCircle : BaseCircle
{
public DrumRollCircle(CirclePiece piece)

View File

@ -26,6 +26,7 @@ namespace osu.Desktop.VisualTests.Tests
AddButton("Hit!", addHitJudgement);
AddButton("Miss :(", addMissJudgement);
AddButton("Swell", addSwell);
AddButton("Centre", () => addCentreHit(false));
AddButton("Strong Centre", () => addCentreHit(true));
AddButton("Rim", () => addRimHit(false));
@ -74,6 +75,16 @@ namespace osu.Desktop.VisualTests.Tests
});
}
private void addSwell()
{
playfield.Add(new DrawableSwell(new Swell
{
StartTime = Time.Current + 1000,
EndTime = Time.Current + 5000,
PreEmpt = 1000
}));
}
private void addCentreHit(bool strong)
{
Hit h = new Hit

View File

@ -11,10 +11,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Game.Modes.Objects.Types;
using osu.Game.Modes.Replays;
namespace osu.Game.Modes.Osu
{
public class OsuAutoReplay : LegacyReplay
public class OsuAutoReplay : Replay
{
private static readonly Vector2 spinner_centre = new Vector2(256, 192);
@ -29,17 +30,20 @@ namespace osu.Game.Modes.Osu
createAutoReplay();
}
private class LegacyReplayFrameComparer : IComparer<LegacyReplayFrame>
private class ReplayFrameComparer : IComparer<ReplayFrame>
{
public int Compare(LegacyReplayFrame f1, LegacyReplayFrame f2)
public int Compare(ReplayFrame f1, ReplayFrame f2)
{
if (f1 == null) throw new NullReferenceException($@"{nameof(f1)} cannot be null");
if (f2 == null) throw new NullReferenceException($@"{nameof(f2)} cannot be null");
return f1.Time.CompareTo(f2.Time);
}
}
private static readonly IComparer<LegacyReplayFrame> replay_frame_comparer = new LegacyReplayFrameComparer();
private static readonly IComparer<ReplayFrame> replay_frame_comparer = new ReplayFrameComparer();
private int findInsertionIndex(LegacyReplayFrame frame)
private int findInsertionIndex(ReplayFrame frame)
{
int index = Frames.BinarySearch(frame, replay_frame_comparer);
@ -59,7 +63,7 @@ namespace osu.Game.Modes.Osu
return index;
}
private void addFrameToReplay(LegacyReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame);
private void addFrameToReplay(ReplayFrame frame) => Frames.Insert(findInsertionIndex(frame), frame);
private static Vector2 circlePosition(double t, double radius) => new Vector2((float)(Math.Cos(t) * radius), (float)(Math.Sin(t) * radius));
@ -74,9 +78,9 @@ namespace osu.Game.Modes.Osu
EasingTypes preferredEasing = DelayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out;
addFrameToReplay(new LegacyReplayFrame(-100000, 256, 500, LegacyButtonState.None));
addFrameToReplay(new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, LegacyButtonState.None));
addFrameToReplay(new LegacyReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, LegacyButtonState.None));
addFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None));
addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None));
addFrameToReplay(new ReplayFrame(beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None));
// We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps.
float frameDelay = (float)applyModsToRate(1000.0 / 60.0);
@ -106,18 +110,18 @@ namespace osu.Game.Modes.Osu
//Make the cursor stay at a hitObject as long as possible (mainly for autopilot).
if (h.StartTime - h.HitWindowFor(OsuScoreResult.Miss) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new LegacyReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, LegacyButtonState.None));
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Miss), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50) > endTime + h.HitWindowFor(OsuScoreResult.Hit50) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new LegacyReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, LegacyButtonState.None));
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit50), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit50), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
else if (h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100) > endTime + h.HitWindowFor(OsuScoreResult.Hit100) + 50)
{
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new LegacyReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), last.EndPosition.X, last.EndPosition.Y, LegacyButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new LegacyReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, LegacyButtonState.None));
if (!(last is Spinner) && h.StartTime - endTime < 1000) addFrameToReplay(new ReplayFrame(endTime + h.HitWindowFor(OsuScoreResult.Hit100), last.EndPosition.X, last.EndPosition.Y, ReplayButtonState.None));
if (!(h is Spinner)) addFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindowFor(OsuScoreResult.Hit100), h.Position.X, h.Position.Y, ReplayButtonState.None));
}
}
@ -173,13 +177,13 @@ namespace osu.Game.Modes.Osu
// Do some nice easing for cursor movements
if (Frames.Count > 0)
{
LegacyReplayFrame lastFrame = Frames[Frames.Count - 1];
ReplayFrame lastFrame = Frames[Frames.Count - 1];
// Wait until Auto could "see and react" to the next note.
double waitTime = h.StartTime - Math.Max(0.0, DrawableOsuHitObject.TIME_PREEMPT - reactionTime);
if (waitTime > lastFrame.Time)
{
lastFrame = new LegacyReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState);
lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState);
addFrameToReplay(lastFrame);
}
@ -196,7 +200,7 @@ namespace osu.Game.Modes.Osu
for (double time = lastFrame.Time + frameDelay; time < h.StartTime; time += frameDelay)
{
Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPosition, lastFrame.Time, h.StartTime, easing);
addFrameToReplay(new LegacyReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState));
addFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState));
}
buttonIndex = 0;
@ -207,12 +211,12 @@ namespace osu.Game.Modes.Osu
}
}
LegacyButtonState button = buttonIndex % 2 == 0 ? LegacyButtonState.Left1 : LegacyButtonState.Right1;
ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1;
double hEndTime = (h as IHasEndTime)?.EndTime ?? h.StartTime;
LegacyReplayFrame newFrame = new LegacyReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button);
LegacyReplayFrame endFrame = new LegacyReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, LegacyButtonState.None);
ReplayFrame newFrame = new ReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button);
ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, ReplayButtonState.None);
// Decrement because we want the previous frame, not the next one
int index = findInsertionIndex(newFrame) - 1;
@ -220,19 +224,19 @@ namespace osu.Game.Modes.Osu
// Do we have a previous frame? No need to check for < replay.Count since we decremented!
if (index >= 0)
{
LegacyReplayFrame previousFrame = Frames[index];
ReplayFrame previousFrame = Frames[index];
var previousButton = previousFrame.ButtonState;
// If a button is already held, then we simply alternate
if (previousButton != LegacyButtonState.None)
if (previousButton != ReplayButtonState.None)
{
Debug.Assert(previousButton != (LegacyButtonState.Left1 | LegacyButtonState.Right1));
Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1));
// Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button.
if (previousButton == button)
{
button = (LegacyButtonState.Left1 | LegacyButtonState.Right1) & ~button;
newFrame.SetButtonStates(button);
button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button;
newFrame.ButtonState = button;
}
// We always follow the most recent slider / spinner, so remove any other frames that occur while it exists.
@ -246,7 +250,7 @@ namespace osu.Game.Modes.Osu
{
// Don't affect frames which stop pressing a button!
if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton)
Frames[j].SetButtonStates(button);
Frames[j].ButtonState = button;
}
}
}
@ -270,13 +274,13 @@ namespace osu.Game.Modes.Osu
t = applyModsToTime(j - h.StartTime) * spinnerDirection;
Vector2 pos = spinner_centre + circlePosition(t / 20 + angle, spin_radius);
addFrameToReplay(new LegacyReplayFrame((int)j, pos.X, pos.Y, button));
addFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button));
}
t = applyModsToTime(s.EndTime - h.StartTime) * spinnerDirection;
Vector2 endPosition = spinner_centre + circlePosition(t / 20 + angle, spin_radius);
addFrameToReplay(new LegacyReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button));
addFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button));
endFrame.MouseX = endPosition.X;
endFrame.MouseY = endPosition.Y;
@ -288,10 +292,10 @@ namespace osu.Game.Modes.Osu
for (double j = frameDelay; j < s.Duration; j += frameDelay)
{
Vector2 pos = s.PositionAt(j / s.Duration);
addFrameToReplay(new LegacyReplayFrame(h.StartTime + j, pos.X, pos.Y, button));
addFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button));
}
addFrameToReplay(new LegacyReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button));
addFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button));
}
// We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed!

View File

@ -1,37 +1,160 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Modes.Objects.Drawables;
using osu.Game.Modes.Taiko.Judgements;
using osu.Game.Modes.Taiko.Objects.Drawable.Pieces;
using System;
using System.Linq;
namespace osu.Game.Modes.Taiko.Objects.Drawable
{
public class DrawableSwell : DrawableTaikoHitObject
{
/// <summary>
/// Invoked when the swell has reached the hit target, i.e. when CurrentTime >= StartTime.
/// This is only ever invoked once.
/// </summary>
public event Action OnStart;
private const float target_ring_thick_border = 1.4f;
private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f;
private const float inner_ring_alpha = 0.65f;
private readonly Swell swell;
private readonly Container bodyContainer;
private readonly CircularContainer targetRing;
private readonly CircularContainer expandingRing;
private readonly CirclePiece circlePiece;
private readonly Key[] rimKeys = { Key.D, Key.K };
private readonly Key[] centreKeys = { Key.F, Key.J };
private Key[] lastKeySet;
/// <summary>
/// The amount of times the user has hit this swell.
/// </summary>
private int userHits;
private readonly Swell swell;
private bool hasStarted;
private readonly SwellSymbolPiece symbol;
public DrawableSwell(Swell swell)
: base(swell)
{
this.swell = swell;
Children = new Framework.Graphics.Drawable[]
{
bodyContainer = new Container
{
Children = new Framework.Graphics.Drawable[]
{
expandingRing = new CircularContainer
{
Name = "Expanding ring",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Size = new Vector2(TaikoHitObject.CIRCLE_RADIUS * 2),
BlendingMode = BlendingMode.Additive,
Masking = true,
Children = new []
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = inner_ring_alpha,
}
}
},
targetRing = new CircularContainer
{
Name = "Target ring (thick border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(TaikoHitObject.CIRCLE_RADIUS * 2),
Masking = true,
BorderThickness = target_ring_thick_border,
BlendingMode = BlendingMode.Additive,
Children = new Framework.Graphics.Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new CircularContainer
{
Name = "Target ring (thin border)",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = target_ring_thin_border,
BorderColour = Color4.White,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
}
},
circlePiece = new CirclePiece
{
Children = new []
{
symbol = new SwellSymbolPiece()
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
circlePiece.AccentColour = colours.YellowDark;
expandingRing.Colour = colours.YellowLight;
targetRing.BorderColour = colours.YellowDark.Opacity(0.25f);
}
protected override void CheckJudgement(bool userTriggered)
{
if (userTriggered)
{
if (Time.Current < HitObject.StartTime)
return;
userHits++;
var completion = (float)userHits / swell.RequiredHits;
expandingRing.FadeTo(expandingRing.Alpha + MathHelper.Clamp(completion / 16, 0.1f, 0.6f), 50);
expandingRing.Delay(50);
expandingRing.FadeTo(completion / 8, 2000, EasingTypes.OutQuint);
expandingRing.DelayReset();
symbol.RotateTo((float)(completion * swell.Duration / 8), 4000, EasingTypes.OutQuint);
expandingRing.ScaleTo(1f + Math.Min(target_ring_scale - 1f, (target_ring_scale - 1f) * completion * 1.3f), 260, EasingTypes.OutQuint);
if (userHits == swell.RequiredHits)
{
Judgement.Result = HitResult.Hit;
@ -43,6 +166,7 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
if (Judgement.TimeOffset < 0)
return;
//TODO: THIS IS SHIT AND CAN'T EXIST POST-TAIKO WORLD CUP
if (userHits > swell.RequiredHits / 2)
{
Judgement.Result = HitResult.Hit;
@ -55,11 +179,42 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
protected override void UpdateState(ArmedState state)
{
const float preempt = 100;
Delay(HitObject.StartTime - Time.Current - preempt, true);
targetRing.ScaleTo(target_ring_scale, preempt * 4, EasingTypes.OutQuint);
Delay(preempt, true);
Delay(Judgement.TimeOffset + swell.Duration, true);
const float out_transition_time = 300;
switch (state)
{
case ArmedState.Hit:
bodyContainer.ScaleTo(1.4f, out_transition_time);
break;
}
FadeOut(out_transition_time, EasingTypes.Out);
Expire();
}
protected override void UpdateScrollPosition(double time)
{
base.UpdateScrollPosition(Math.Min(time, HitObject.StartTime));
// Make the swell stop at the hit target
double t = Math.Min(HitObject.StartTime, time);
if (t == HitObject.StartTime && !hasStarted)
{
OnStart?.Invoke();
hasStarted = true;
}
base.UpdateScrollPosition(t);
}
protected override bool HandleKeyPress(Key key)
@ -67,6 +222,18 @@ namespace osu.Game.Modes.Taiko.Objects.Drawable
if (Judgement.Result.HasValue)
return false;
// Don't handle keys before the swell starts
if (Time.Current < HitObject.StartTime)
return false;
// Find the keyset which this key corresponds to
var keySet = rimKeys.Contains(key) ? rimKeys : centreKeys;
// Ensure alternating keysets
if (keySet == lastKeySet)
return false;
lastKeySet = keySet;
UpdateJudgement(true);
return true;

View File

@ -0,0 +1,24 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Graphics;
using osu.Framework.Graphics;
namespace osu.Game.Modes.Taiko.Objects.Drawable.Pieces
{
/// <summary>
/// The symbol used for swell pieces.
/// </summary>
public class SwellSymbolPiece : TextAwesome
{
public SwellSymbolPiece()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
UseFullGlyphHeight = true;
TextSize = CirclePiece.SYMBOL_INNER_SIZE;
Icon = FontAwesome.fa_asterisk;
Shadow = false;
}
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Modes.Taiko.Objects
/// <summary>
/// The number of hits required to complete the swell successfully.
/// </summary>
public int RequiredHits { get; protected set; }
public int RequiredHits { get; protected set; } = 10;
public override void ApplyDefaults(TimingInfo timing, BeatmapDifficulty difficulty)
{

View File

@ -14,6 +14,7 @@ using osu.Game.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Primitives;
using osu.Game.Modes.Taiko.Objects.Drawable;
namespace osu.Game.Modes.Taiko.UI
{
@ -42,7 +43,7 @@ namespace osu.Game.Modes.Taiko.UI
private readonly Container<DrawableTaikoJudgement> judgementContainer;
private readonly Container hitObjectContainer;
//private Container topLevelHitContainer;
private readonly Container topLevelHitContainer;
private readonly Container leftBackgroundContainer;
private readonly Container rightBackgroundContainer;
private readonly Box leftBackground;
@ -143,10 +144,10 @@ namespace osu.Game.Modes.Taiko.UI
},
}
},
//topLevelHitContainer = new Container
//{
// RelativeSizeAxes = Axes.Both,
//}
topLevelHitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
}
});
}
@ -165,6 +166,11 @@ namespace osu.Game.Modes.Taiko.UI
h.Depth = (float)h.HitObject.StartTime;
base.Add(h);
// Swells should be moved at the very top of the playfield when they reach the hit target
var swell = h as DrawableSwell;
if (swell != null)
swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy());
}
public override void OnJudgement(DrawableHitObject<TaikoHitObject, TaikoJudgement> judgedObject)

View File

@ -68,6 +68,7 @@
<Compile Include="Objects\Drawable\Pieces\RimHitSymbolPiece.cs" />
<Compile Include="Objects\Drawable\Pieces\StrongCirclePiece.cs" />
<Compile Include="Objects\Drawable\Pieces\CirclePiece.cs" />
<Compile Include="Objects\Drawable\Pieces\SwellSymbolPiece.cs" />
<Compile Include="Objects\DrumRoll.cs" />
<Compile Include="Objects\DrumRollTick.cs" />
<Compile Include="Objects\Hit.cs" />

View File

@ -101,7 +101,7 @@ namespace osu.Game.Database
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
using (var reader = new StreamReader(lzma))
score.Replay = score.CreateLegacyReplayFrom(reader);
score.Replay = score.CreateReplay(reader);
}
}

View File

@ -47,7 +47,7 @@ namespace osu.Game.Graphics.UserInterface
protected class TransformAccuracy : Transform<double>
{
protected override double CurrentValue
public override double CurrentValue
{
get
{

View File

@ -51,7 +51,7 @@ namespace osu.Game.Graphics.UserInterface
protected class TransformScore : Transform<double>
{
protected override double CurrentValue
public override double CurrentValue
{
get
{

View File

@ -1,268 +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;
using System.Collections.Generic;
using System.IO;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Input.Handlers;
using osu.Game.IO.Legacy;
using OpenTK;
using OpenTK.Input;
using KeyboardState = osu.Framework.Input.KeyboardState;
using MouseState = osu.Framework.Input.MouseState;
namespace osu.Game.Modes
{
public class LegacyReplay : Replay
{
protected List<LegacyReplayFrame> Frames = new List<LegacyReplayFrame>();
protected LegacyReplay()
{
}
public LegacyReplay(StreamReader reader)
{
float lastTime = 0;
foreach (var l in reader.ReadToEnd().Split(','))
{
var split = l.Split('|');
if (split.Length < 4 || float.Parse(split[0]) < 0) continue;
lastTime += float.Parse(split[0]);
Frames.Add(new LegacyReplayFrame(
lastTime,
float.Parse(split[1]),
384 - float.Parse(split[2]),
(LegacyButtonState)int.Parse(split[3])
));
}
}
public override ReplayInputHandler CreateInputHandler() => new LegacyReplayInputHandler(Frames);
/// <summary>
/// The ReplayHandler will take a replay and handle the propagation of updates to the input stack.
/// It handles logic of any frames which *must* be executed.
/// </summary>
protected class LegacyReplayInputHandler : ReplayInputHandler
{
private readonly List<LegacyReplayFrame> replayContent;
public LegacyReplayFrame CurrentFrame => !hasFrames ? null : replayContent[currentFrameIndex];
public LegacyReplayFrame NextFrame => !hasFrames ? null : replayContent[nextFrameIndex];
private int currentFrameIndex;
private int nextFrameIndex => MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, replayContent.Count - 1);
public LegacyReplayInputHandler(List<LegacyReplayFrame> replayContent)
{
this.replayContent = replayContent;
}
private bool advanceFrame()
{
int newFrame = nextFrameIndex;
//ensure we aren't at an extent.
if (newFrame == currentFrameIndex) return false;
currentFrameIndex = newFrame;
return true;
}
public void SetPosition(Vector2 pos)
{
}
private Vector2? position
{
get
{
if (!hasFrames)
return null;
return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time);
}
}
public override List<InputState> GetPendingStates()
{
var buttons = new HashSet<MouseButton>();
if (CurrentFrame?.MouseLeft ?? false)
buttons.Add(MouseButton.Left);
if (CurrentFrame?.MouseRight ?? false)
buttons.Add(MouseButton.Right);
return new List<InputState>
{
new InputState
{
Mouse = new ReplayMouseState(ToScreenSpace(position ?? Vector2.Zero), buttons),
Keyboard = new ReplayKeyboardState(new List<Key>())
}
};
}
public bool AtLastFrame => currentFrameIndex == replayContent.Count - 1;
public bool AtFirstFrame => currentFrameIndex == 0;
public Vector2 Size => new Vector2(512, 384);
private const double sixty_frame_time = 1000.0 / 60;
private double currentTime;
private int currentDirection;
/// <summary>
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently).
/// </summary>
public bool FrameAccuratePlayback = true;
private bool hasFrames => replayContent.Count > 0;
private bool inImportantSection =>
FrameAccuratePlayback &&
//a button is in a pressed state
(currentDirection > 0 ? CurrentFrame : NextFrame)?.ButtonState > LegacyButtonState.None &&
//the next frame is within an allowable time span
Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
/// <summary>
/// Update the current frame based on an incoming time value.
/// There are cases where we return a "must-use" time value that is different from the input.
/// This is to ensure accurate playback of replay data.
/// </summary>
/// <param name="time">The time which we should use for finding the current frame.</param>
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime(double time)
{
currentDirection = time.CompareTo(currentTime);
if (currentDirection == 0) currentDirection = 1;
if (hasFrames)
{
//if we changed frames, we want to execute once *exactly* on the frame's time.
if (currentDirection == time.CompareTo(NextFrame.Time) && advanceFrame())
return currentTime = CurrentFrame.Time;
//if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null.
if (inImportantSection)
return null;
}
return currentTime = time;
}
protected class ReplayMouseState : MouseState
{
public ReplayMouseState(Vector2 position, IEnumerable<MouseButton> list)
{
Position = position;
list.ForEach(b => PressedButtons.Add(b));
}
}
protected class ReplayKeyboardState : KeyboardState
{
public ReplayKeyboardState(List<Key> keys)
{
Keys = keys;
}
}
}
[Flags]
protected enum LegacyButtonState
{
None = 0,
Left1 = 1,
Right1 = 2,
Left2 = 4,
Right2 = 8,
Smoke = 16
}
protected class LegacyReplayFrame
{
public Vector2 Position => new Vector2(MouseX, MouseY);
public float MouseX;
public float MouseY;
public bool MouseLeft;
public bool MouseRight;
public bool MouseLeft1;
public bool MouseRight1;
public bool MouseLeft2;
public bool MouseRight2;
public LegacyButtonState ButtonState;
public double Time;
public LegacyReplayFrame(double time, float posX, float posY, LegacyButtonState buttonState)
{
MouseX = posX;
MouseY = posY;
ButtonState = buttonState;
SetButtonStates(buttonState);
Time = time;
}
public void SetButtonStates(LegacyButtonState buttonState)
{
ButtonState = buttonState;
MouseLeft = (buttonState & (LegacyButtonState.Left1 | LegacyButtonState.Left2)) > 0;
MouseLeft1 = (buttonState & LegacyButtonState.Left1) > 0;
MouseLeft2 = (buttonState & LegacyButtonState.Left2) > 0;
MouseRight = (buttonState & (LegacyButtonState.Right1 | LegacyButtonState.Right2)) > 0;
MouseRight1 = (buttonState & LegacyButtonState.Right1) > 0;
MouseRight2 = (buttonState & LegacyButtonState.Right2) > 0;
}
public LegacyReplayFrame(Stream s) : this(new SerializationReader(s))
{
}
public LegacyReplayFrame(SerializationReader sr)
{
ButtonState = (LegacyButtonState)sr.ReadByte();
SetButtonStates(ButtonState);
byte bt = sr.ReadByte();
if (bt > 0)//Handle Pre-Taiko compatible replays.
SetButtonStates(LegacyButtonState.Right1);
MouseX = sr.ReadSingle();
MouseY = sr.ReadSingle();
Time = sr.ReadInt32();
}
public void ReadFromStream(SerializationReader sr)
{
throw new NotImplementedException();
}
public void WriteToStream(SerializationWriter sw)
{
sw.Write((byte)ButtonState);
sw.Write((byte)0);
sw.Write(MouseX);
sw.Write(MouseY);
sw.Write(Time);
}
public override string ToString()
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
}
}
}

View File

@ -157,7 +157,7 @@ namespace osu.Game.Modes.Mods
public void Apply(HitRenderer<T> hitRenderer)
{
hitRenderer.InputManager.ReplayInputHandler = CreateReplayScore(hitRenderer.Beatmap)?.Replay?.CreateInputHandler();
hitRenderer.SetReplay(CreateReplayScore(hitRenderer.Beatmap)?.Replay);
}
}

View File

@ -1,12 +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 osu.Game.Input.Handlers;
namespace osu.Game.Modes
{
public abstract class Replay
{
public virtual ReplayInputHandler CreateInputHandler() => null;
}
}

View File

@ -0,0 +1,151 @@
// 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;
using System.Collections.Generic;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Input.Handlers;
using OpenTK;
using OpenTK.Input;
using KeyboardState = osu.Framework.Input.KeyboardState;
using MouseState = osu.Framework.Input.MouseState;
namespace osu.Game.Modes.Replays
{
/// <summary>
/// The ReplayHandler will take a replay and handle the propagation of updates to the input stack.
/// It handles logic of any frames which *must* be executed.
/// </summary>
public class FramedReplayInputHandler : ReplayInputHandler
{
private readonly Replay replay;
protected List<ReplayFrame> Frames => replay.Frames;
public ReplayFrame CurrentFrame => !hasFrames ? null : Frames[currentFrameIndex];
public ReplayFrame NextFrame => !hasFrames ? null : Frames[nextFrameIndex];
private int currentFrameIndex;
private int nextFrameIndex => MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, Frames.Count - 1);
public FramedReplayInputHandler(Replay replay)
{
this.replay = replay;
}
private bool advanceFrame()
{
int newFrame = nextFrameIndex;
//ensure we aren't at an extent.
if (newFrame == currentFrameIndex) return false;
currentFrameIndex = newFrame;
return true;
}
public void SetPosition(Vector2 pos)
{
}
private Vector2? position
{
get
{
if (!hasFrames)
return null;
return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time);
}
}
public override List<InputState> GetPendingStates()
{
var buttons = new HashSet<MouseButton>();
if (CurrentFrame?.MouseLeft ?? false)
buttons.Add(MouseButton.Left);
if (CurrentFrame?.MouseRight ?? false)
buttons.Add(MouseButton.Right);
return new List<InputState>
{
new InputState
{
Mouse = new ReplayMouseState(ToScreenSpace(position ?? Vector2.Zero), buttons),
Keyboard = new ReplayKeyboardState(new List<Key>())
}
};
}
public bool AtLastFrame => currentFrameIndex == Frames.Count - 1;
public bool AtFirstFrame => currentFrameIndex == 0;
public Vector2 Size => new Vector2(512, 384);
private const double sixty_frame_time = 1000.0 / 60;
private double currentTime;
private int currentDirection;
/// <summary>
/// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data.
/// Disabling this can make replay playback smoother (useful for autoplay, currently).
/// </summary>
public bool FrameAccuratePlayback = true;
private bool hasFrames => Frames.Count > 0;
private bool inImportantSection =>
FrameAccuratePlayback &&
//a button is in a pressed state
((currentDirection > 0 ? CurrentFrame : NextFrame)?.IsImportant ?? false) &&
//the next frame is within an allowable time span
Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
/// <summary>
/// Update the current frame based on an incoming time value.
/// There are cases where we return a "must-use" time value that is different from the input.
/// This is to ensure accurate playback of replay data.
/// </summary>
/// <param name="time">The time which we should use for finding the current frame.</param>
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime(double time)
{
currentDirection = time.CompareTo(currentTime);
if (currentDirection == 0) currentDirection = 1;
if (hasFrames)
{
//if we changed frames, we want to execute once *exactly* on the frame's time.
if (currentDirection == time.CompareTo(NextFrame.Time) && advanceFrame())
return currentTime = CurrentFrame.Time;
//if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null.
if (inImportantSection)
return null;
}
return currentTime = time;
}
protected class ReplayMouseState : MouseState
{
public ReplayMouseState(Vector2 position, IEnumerable<MouseButton> list)
{
Position = position;
list.ForEach(b => PressedButtons.Add(b));
}
}
protected class ReplayKeyboardState : KeyboardState
{
public ReplayKeyboardState(List<Key> keys)
{
Keys = keys;
}
}
}
}

View File

@ -0,0 +1,12 @@
// 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.Modes.Replays
{
public class Replay
{
public List<ReplayFrame> Frames = new List<ReplayFrame>();
}
}

View File

@ -0,0 +1,18 @@
// 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;
namespace osu.Game.Modes.Replays
{
[Flags]
public enum ReplayButtonState
{
None = 0,
Left1 = 1,
Right1 = 2,
Left2 = 4,
Right2 = 8,
Smoke = 16
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
namespace osu.Game.Modes.Replays
{
public class ReplayFrame
{
public Vector2 Position => new Vector2(MouseX, MouseY);
public bool IsImportant => MouseLeft || MouseRight;
public float MouseX;
public float MouseY;
public bool MouseLeft => MouseLeft1 || MouseLeft2;
public bool MouseRight => MouseRight1 || MouseRight2;
public bool MouseLeft1
{
get { return (ButtonState & ReplayButtonState.Left1) > 0; }
set { setButtonState(ReplayButtonState.Left1, value); }
}
public bool MouseRight1
{
get { return (ButtonState & ReplayButtonState.Right1) > 0; }
set { setButtonState(ReplayButtonState.Right1, value); }
}
public bool MouseLeft2
{
get { return (ButtonState & ReplayButtonState.Left2) > 0; }
set { setButtonState(ReplayButtonState.Left2, value); }
}
public bool MouseRight2
{
get { return (ButtonState & ReplayButtonState.Right2) > 0; }
set { setButtonState(ReplayButtonState.Right2, value); }
}
private void setButtonState(ReplayButtonState singleButton, bool pressed)
{
if (pressed)
ButtonState |= singleButton;
else
ButtonState &= ~singleButton;
}
public double Time;
public ReplayButtonState ButtonState;
protected ReplayFrame()
{
}
public ReplayFrame(double time, float posX, float posY, ReplayButtonState buttonState)
{
MouseX = posX;
MouseY = posY;
ButtonState = buttonState;
Time = time;
}
public override string ToString()
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
}
}

View File

@ -2,11 +2,13 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using osu.Game.Database;
using osu.Game.Modes.Mods;
using osu.Game.Users;
using System.IO;
using osu.Game.Modes.Replays;
namespace osu.Game.Modes.Scoring
{
@ -45,11 +47,34 @@ namespace osu.Game.Modes.Scoring
public DateTime Date;
/// <summary>
/// Creates a legacy replay which is read from a stream.
/// Creates a replay which is read from a stream.
/// </summary>
/// <param name="reader">The stream reader.</param>
/// <returns>The replay.</returns>
public virtual Replay CreateLegacyReplayFrom(StreamReader reader) => new LegacyReplay(reader);
public virtual Replay CreateReplay(StreamReader reader)
{
var frames = new List<ReplayFrame>();
float lastTime = 0;
foreach (var l in reader.ReadToEnd().Split(','))
{
var split = l.Split('|');
if (split.Length < 4 || float.Parse(split[0]) < 0) continue;
lastTime += float.Parse(split[0]);
frames.Add(new ReplayFrame(
lastTime,
float.Parse(split[1]),
384 - float.Parse(split[2]),
(ReplayButtonState)int.Parse(split[3])
));
}
return new Replay { Frames = frames };
}
// [JsonProperty(@"count50")] 0,
//[JsonProperty(@"count100")] 0,

View File

@ -213,7 +213,7 @@ namespace osu.Game.Modes.UI
protected class TransformComboRoll : Transform<int>
{
protected override int CurrentValue
public override int CurrentValue
{
get
{

View File

@ -36,7 +36,7 @@ namespace osu.Game.Modes.UI
protected class TransformComboResult : Transform<ulong>
{
protected override ulong CurrentValue
public override ulong CurrentValue
{
get
{

View File

@ -14,6 +14,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Game.Modes.Replays;
using osu.Game.Modes.Scoring;
namespace osu.Game.Modes.UI
@ -68,6 +69,14 @@ namespace osu.Game.Modes.UI
/// </summary>
/// <returns>The input manager.</returns>
protected virtual KeyConversionInputManager CreateKeyConversionInputManager() => new KeyConversionInputManager();
protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new FramedReplayInputHandler(replay);
/// <summary>
/// Sets a replay to be used, overriding local input.
/// </summary>
/// <param name="replay">The replay, null for local input.</param>
public void SetReplay(Replay replay) => InputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null;
}
/// <summary>

View File

@ -125,7 +125,7 @@ namespace osu.Game
Beatmap.Value = BeatmapDatabase.GetWorkingBeatmap(s.Beatmap);
menu.Push(new PlayerLoader(new Player { ReplayInputHandler = s.Replay.CreateInputHandler() }));
menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay)));
}
protected override void LoadComplete()

View File

@ -15,7 +15,6 @@ using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Input.Handlers;
using osu.Game.Modes;
using osu.Game.Modes.UI;
using osu.Game.Screens.Backgrounds;
@ -34,7 +33,7 @@ namespace osu.Game.Screens.Play
internal override bool HasLocalCursorDisplayed => !hasReplayLoaded && !IsPaused;
private bool hasReplayLoaded => hitRenderer.InputManager.ReplayInputHandler != null;
private bool hasReplayLoaded => HitRenderer.InputManager.ReplayInputHandler != null;
public BeatmapInfo BeatmapInfo;
@ -53,7 +52,7 @@ namespace osu.Game.Screens.Play
private Ruleset ruleset;
private ScoreProcessor scoreProcessor;
private HitRenderer hitRenderer;
protected HitRenderer HitRenderer;
private Bindable<int> dimLevel;
private SkipButton skipButton;
@ -112,9 +111,9 @@ namespace osu.Game.Screens.Play
});
ruleset = Ruleset.GetRuleset(Beatmap.PlayMode);
hitRenderer = ruleset.CreateHitRendererWith(Beatmap);
HitRenderer = ruleset.CreateHitRendererWith(Beatmap);
scoreProcessor = hitRenderer.CreateScoreProcessor();
scoreProcessor = HitRenderer.CreateScoreProcessor();
hudOverlay = new StandardHudOverlay();
hudOverlay.KeyCounter.Add(ruleset.CreateGameplayKeys());
@ -133,13 +132,10 @@ namespace osu.Game.Screens.Play
};
if (ReplayInputHandler != null)
hitRenderer.InputManager.ReplayInputHandler = ReplayInputHandler;
hudOverlay.BindHitRenderer(hitRenderer);
hudOverlay.BindHitRenderer(HitRenderer);
//bind HitRenderer to ScoreProcessor and ourselves (for a pass situation)
hitRenderer.OnAllJudged += onCompletion;
HitRenderer.OnAllJudged += onCompletion;
//bind ScoreProcessor to ourselves (for a fail situation)
scoreProcessor.Failed += onFail;
@ -152,7 +148,7 @@ namespace osu.Game.Screens.Play
Clock = interpolatedSourceClock,
Children = new Drawable[]
{
hitRenderer,
HitRenderer,
skipButton = new SkipButton
{
Alpha = 0
@ -336,8 +332,6 @@ namespace osu.Game.Screens.Play
private Bindable<bool> mouseWheelDisabled;
public ReplayInputHandler ReplayInputHandler;
protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !IsPaused;
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Modes.Replays;
namespace osu.Game.Screens.Play
{
public class ReplayPlayer : Player
{
public Replay Replay;
public ReplayPlayer(Replay replay)
{
Replay = replay;
}
protected override void LoadComplete()
{
base.LoadComplete();
HitRenderer.SetReplay(Replay);
}
}
}

View File

@ -96,8 +96,9 @@
<Compile Include="IO\Legacy\SerializationReader.cs" />
<Compile Include="IO\Legacy\SerializationWriter.cs" />
<Compile Include="IPC\ScoreIPCChannel.cs" />
<Compile Include="Modes\Replays\Replay.cs" />
<Compile Include="Modes\Judgements\DrawableJudgement.cs" />
<Compile Include="Modes\LegacyReplay.cs" />
<Compile Include="Modes\Replays\FramedReplayInputHandler.cs" />
<Compile Include="Modes\Mods\IApplicableMod.cs" />
<Compile Include="Modes\Mods\ModType.cs" />
<Compile Include="Modes\Objects\Drawables\ArmedState.cs" />
@ -123,7 +124,8 @@
<Compile Include="Modes\Objects\Types\IHasPosition.cs" />
<Compile Include="Modes\Objects\Types\IHasHold.cs" />
<Compile Include="Modes\Objects\Legacy\LegacyHitObjectType.cs" />
<Compile Include="Modes\Replay.cs" />
<Compile Include="Modes\Replays\ReplayButtonState.cs" />
<Compile Include="Modes\Replays\ReplayFrame.cs" />
<Compile Include="Modes\Scoring\Score.cs" />
<Compile Include="Modes\Scoring\ScoreProcessor.cs" />
<Compile Include="Modes\UI\HealthDisplay.cs" />
@ -198,6 +200,7 @@
<Compile Include="Screens\Play\KeyConversionInputManager.cs" />
<Compile Include="Screens\Play\PlayerInputManager.cs" />
<Compile Include="Screens\Play\PlayerLoader.cs" />
<Compile Include="Screens\Play\ReplayPlayer.cs" />
<Compile Include="Screens\Play\SkipButton.cs" />
<Compile Include="Modes\UI\StandardComboCounter.cs" />
<Compile Include="Screens\Select\BeatmapCarousel.cs" />