mirror of
https://github.com/ppy/osu.git
synced 2025-02-15 21:42:55 +08:00
Merge pull request #203 from peppy/slider-loading
Add interactivity to sliders, follow circle, (non-buffered) paths etc.
This commit is contained in:
commit
bb37e5d955
@ -1 +1 @@
|
||||
Subproject commit fdea70aee37b040d56fac5e9b27a18ed77f2bfb9
|
||||
Subproject commit e125c03d8c39fd86e02e872a8d46654d2ea2759f
|
@ -40,6 +40,7 @@ namespace osu.Desktop.VisualTests.Tests
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (oldDb != null)
|
||||
Dependencies.Cache(oldDb, true);
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.GameModes.Testing;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Framework.Timing;
|
||||
@ -9,6 +10,8 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using OpenTK;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Modes;
|
||||
using osu.Game.Modes.Objects;
|
||||
using osu.Game.Modes.Osu.Objects;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -18,10 +21,17 @@ namespace osu.Desktop.VisualTests.Tests
|
||||
{
|
||||
class TestCasePlayer : TestCase
|
||||
{
|
||||
private WorkingBeatmap beatmap;
|
||||
public override string Name => @"Player";
|
||||
|
||||
public override string Description => @"Showing everything to play the game.";
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapDatabase db)
|
||||
{
|
||||
beatmap = db.GetWorkingBeatmap(db.Query<BeatmapInfo>().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault());
|
||||
}
|
||||
|
||||
public override void Reset()
|
||||
{
|
||||
base.Reset();
|
||||
@ -29,6 +39,9 @@ namespace osu.Desktop.VisualTests.Tests
|
||||
//ensure we are at offset 0
|
||||
Clock = new FramedClock();
|
||||
|
||||
if (beatmap == null)
|
||||
{
|
||||
|
||||
var objects = new List<HitObject>();
|
||||
|
||||
int time = 1500;
|
||||
@ -54,6 +67,9 @@ namespace osu.Desktop.VisualTests.Tests
|
||||
|
||||
decoder.Process(b);
|
||||
|
||||
beatmap = new WorkingBeatmap(b);
|
||||
}
|
||||
|
||||
Add(new Box
|
||||
{
|
||||
RelativeSizeAxes = Framework.Graphics.Axes.Both,
|
||||
@ -62,7 +78,8 @@ namespace osu.Desktop.VisualTests.Tests
|
||||
|
||||
Add(new Player
|
||||
{
|
||||
Beatmap = new WorkingBeatmap(b)
|
||||
PreferredPlayMode = PlayMode.Osu,
|
||||
Beatmap = beatmap
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -59,15 +59,15 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
Colour = osuObject.Colour,
|
||||
}
|
||||
};
|
||||
|
||||
//may not be so correct
|
||||
Size = circle.DrawSize;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
//may not be so correct
|
||||
Size = circle.DrawSize;
|
||||
|
||||
//force application of the state that was set before we loaded.
|
||||
UpdateState(State);
|
||||
}
|
||||
@ -104,16 +104,9 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
Judgement.Result = HitResult.Miss;
|
||||
}
|
||||
|
||||
protected override void UpdateState(ArmedState state)
|
||||
protected override void UpdateInitialState()
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
|
||||
Flush(true); //move to DrawableHitObject
|
||||
ApproachCircle.Flush(true);
|
||||
|
||||
double t = osuObject.EndTime + Judgement.TimeOffset;
|
||||
|
||||
Alpha = 0;
|
||||
base.UpdateInitialState();
|
||||
|
||||
//sane defaults
|
||||
ring.Alpha = circle.Alpha = number.Alpha = glow.Alpha = 1;
|
||||
@ -121,29 +114,30 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
ApproachCircle.Scale = new Vector2(2);
|
||||
explode.Alpha = 0;
|
||||
Scale = new Vector2(0.5f); //this will probably need to be moved to DrawableHitObject at some point.
|
||||
}
|
||||
|
||||
const float preempt = 600;
|
||||
protected override void UpdatePreemptState()
|
||||
{
|
||||
base.UpdatePreemptState();
|
||||
|
||||
const float fadein = 400;
|
||||
ApproachCircle.FadeIn(Math.Min(TIME_FADEIN * 2, TIME_PREEMPT));
|
||||
ApproachCircle.ScaleTo(0.6f, TIME_PREEMPT);
|
||||
}
|
||||
|
||||
Delay(t - Time.Current - preempt, true);
|
||||
protected override void UpdateState(ArmedState state)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
|
||||
FadeIn(fadein);
|
||||
|
||||
ApproachCircle.FadeIn(Math.Min(fadein * 2, preempt));
|
||||
ApproachCircle.ScaleTo(0.6f, preempt);
|
||||
|
||||
Delay(preempt, true);
|
||||
base.UpdateState(state);
|
||||
|
||||
ApproachCircle.FadeOut();
|
||||
|
||||
glow.FadeOut(400);
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case ArmedState.Idle:
|
||||
Delay(osuObject.Duration + 500);
|
||||
FadeOut(500);
|
||||
Delay(osuObject.Duration + TIME_PREEMPT);
|
||||
FadeOut(TIME_FADEOUT);
|
||||
|
||||
explosion?.Expire();
|
||||
explosion = null;
|
||||
|
@ -11,6 +11,10 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
{
|
||||
public class DrawableOsuHitObject : DrawableHitObject
|
||||
{
|
||||
protected const float TIME_PREEMPT = 600;
|
||||
protected const float TIME_FADEIN = 400;
|
||||
protected const float TIME_FADEOUT = 500;
|
||||
|
||||
public DrawableOsuHitObject(OsuHitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
@ -20,7 +24,27 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
|
||||
protected override void UpdateState(ArmedState state)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
if (!IsLoaded) return;
|
||||
|
||||
Flush(true);
|
||||
|
||||
UpdateInitialState();
|
||||
|
||||
Delay(HitObject.StartTime - Time.Current - TIME_PREEMPT + Judgement.TimeOffset, true);
|
||||
|
||||
UpdatePreemptState();
|
||||
|
||||
Delay(TIME_PREEMPT, true);
|
||||
}
|
||||
|
||||
protected virtual void UpdatePreemptState()
|
||||
{
|
||||
FadeIn(TIME_FADEIN);
|
||||
}
|
||||
|
||||
protected virtual void UpdateInitialState()
|
||||
{
|
||||
Alpha = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,26 +1,52 @@
|
||||
using osu.Framework.Graphics;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Modes.Objects.Drawables;
|
||||
using osu.Game.Modes.Osu.Objects.Drawables.Pieces;
|
||||
using OpenTK;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Transformations;
|
||||
using OpenTK.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using OpenTK.Graphics.ES30;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
{
|
||||
class DrawableSlider : DrawableOsuHitObject
|
||||
{
|
||||
public DrawableSlider(Slider h) : base(h)
|
||||
private Slider slider;
|
||||
|
||||
private DrawableHitCircle startCircle;
|
||||
private Container ball;
|
||||
private Body body;
|
||||
|
||||
public DrawableSlider(Slider s) : base(s)
|
||||
{
|
||||
Origin = Anchor.Centre;
|
||||
Position = new Vector2(h.Position.X, h.Position.Y);
|
||||
slider = s;
|
||||
|
||||
Path sliderPath;
|
||||
Add(sliderPath = new Path());
|
||||
Origin = Anchor.TopLeft;
|
||||
Position = Vector2.Zero;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
for (int i = 0; i < h.Curve.Path.Count; ++i)
|
||||
sliderPath.Positions.Add(h.Curve.Path[i] - h.Position);
|
||||
|
||||
h.Position = Vector2.Zero;
|
||||
Add(new DrawableHitCircle(h));
|
||||
Children = new Drawable[]
|
||||
{
|
||||
body = new Body(s)
|
||||
{
|
||||
Position = s.Position,
|
||||
},
|
||||
ball = new Ball(),
|
||||
startCircle = new DrawableHitCircle(new HitCircle
|
||||
{
|
||||
StartTime = s.StartTime,
|
||||
Position = s.Position,
|
||||
Colour = s.Colour,
|
||||
})
|
||||
{
|
||||
Depth = 1 //override time-based depth.
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -31,19 +57,222 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
|
||||
UpdateState(State);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
ball.Alpha = Time.Current >= slider.StartTime && Time.Current <= slider.EndTime ? 1 : 0;
|
||||
|
||||
double t = (Time.Current - slider.StartTime) / slider.Duration;
|
||||
if (slider.RepeatCount > 1)
|
||||
{
|
||||
int currentRepeat = (int)(t * slider.RepeatCount);
|
||||
t = (t * slider.RepeatCount) % 1;
|
||||
if (currentRepeat % 2 == 1)
|
||||
t = 1 - t;
|
||||
}
|
||||
|
||||
ball.Position = slider.Curve.PositionAt(t);
|
||||
}
|
||||
|
||||
protected override void UpdateState(ArmedState state)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
base.UpdateState(state);
|
||||
|
||||
Flush(true); //move to DrawableHitObject
|
||||
Delay(HitObject.Duration);
|
||||
FadeOut(100);
|
||||
}
|
||||
|
||||
Alpha = 0;
|
||||
private class Ball : Container
|
||||
{
|
||||
private Box follow;
|
||||
|
||||
Delay(HitObject.StartTime - 450 - Time.Current, true);
|
||||
public Ball()
|
||||
{
|
||||
Masking = true;
|
||||
AutoSizeAxes = Axes.Both;
|
||||
BlendingMode = BlendingMode.Additive;
|
||||
Origin = Anchor.Centre;
|
||||
|
||||
FadeIn(200);
|
||||
Delay(450 + HitObject.Duration);
|
||||
FadeOut(200);
|
||||
Children = new Drawable[]
|
||||
{
|
||||
follow = new Box
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Colour = Color4.Orange,
|
||||
Width = 64,
|
||||
Height = 64,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Colour = Color4.Cyan,
|
||||
CornerRadius = 32,
|
||||
Children = new[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
|
||||
Width = 64,
|
||||
Height = 64,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
private InputState lastState;
|
||||
|
||||
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
|
||||
{
|
||||
lastState = state;
|
||||
return base.OnMouseDown(state, args);
|
||||
}
|
||||
|
||||
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
|
||||
{
|
||||
lastState = state;
|
||||
return base.OnMouseUp(state, args);
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(InputState state)
|
||||
{
|
||||
lastState = state;
|
||||
return base.OnMouseMove(state);
|
||||
}
|
||||
|
||||
bool tracking;
|
||||
protected bool Tracking
|
||||
{
|
||||
get { return tracking; }
|
||||
set
|
||||
{
|
||||
if (value == tracking) return;
|
||||
|
||||
tracking = value;
|
||||
|
||||
follow.ScaleTo(tracking ? 2.4f : 1, 140, EasingTypes.Out);
|
||||
follow.FadeTo(tracking ? 0.8f : 0, 140, EasingTypes.Out);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
CornerRadius = DrawWidth / 2;
|
||||
Tracking = lastState != null && Contains(lastState.Mouse.NativeState.Position) && lastState.Mouse.HasMainButtonPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private class Body : Container
|
||||
{
|
||||
private Path path;
|
||||
private BufferedContainer container;
|
||||
|
||||
private double? drawnProgress;
|
||||
|
||||
private Slider slider;
|
||||
public Body(Slider s)
|
||||
{
|
||||
slider = s;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
container = new BufferedContainer
|
||||
{
|
||||
CacheDrawnFrameBuffer = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
path = new Path
|
||||
{
|
||||
Colour = s.Colour,
|
||||
BlendingMode = BlendingMode.None,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
container.Attach(RenderbufferInternalFormat.DepthComponent16);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
// Surprisingly, this looks somewhat okay and works well as a test for self-overlaps.
|
||||
// TODO: Don't do this.
|
||||
path.Texture = textures.Get(@"Menu/logo");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
path.PathWidth = 32;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (updateSnaking())
|
||||
{
|
||||
// Autosizing does not give us the desired behaviour here.
|
||||
// We want the container to have the same size as the slider,
|
||||
// and to be positioned such that the slider head is at (0,0).
|
||||
container.Size = path.Size;
|
||||
container.Position = -path.HeadPosition;
|
||||
|
||||
container.ForceRedraw();
|
||||
}
|
||||
}
|
||||
|
||||
private bool updateSnaking()
|
||||
{
|
||||
double progress = MathHelper.Clamp((Time.Current - slider.StartTime + TIME_PREEMPT) / TIME_FADEIN, 0, 1);
|
||||
|
||||
if (progress == drawnProgress) return false;
|
||||
|
||||
bool madeChanges = false;
|
||||
if (progress == 0)
|
||||
{
|
||||
//if we have gone backwards, just clear the path for now.
|
||||
drawnProgress = 0;
|
||||
path.ClearVertices();
|
||||
madeChanges = true;
|
||||
}
|
||||
|
||||
Vector2 startPosition = slider.Curve.PositionAt(0);
|
||||
|
||||
if (drawnProgress == null)
|
||||
{
|
||||
drawnProgress = 0;
|
||||
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
|
||||
madeChanges = true;
|
||||
}
|
||||
|
||||
double segmentSize = 1 / (slider.Curve.Length / 5);
|
||||
|
||||
while (drawnProgress + segmentSize < progress)
|
||||
{
|
||||
drawnProgress += segmentSize;
|
||||
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
|
||||
madeChanges = true;
|
||||
}
|
||||
|
||||
if (progress == 1 && drawnProgress != progress)
|
||||
{
|
||||
drawnProgress = progress;
|
||||
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
|
||||
madeChanges = true;
|
||||
}
|
||||
|
||||
return madeChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,18 +2,27 @@
|
||||
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Database;
|
||||
using OpenTK;
|
||||
using osu.Game.Beatmaps;
|
||||
using System;
|
||||
|
||||
namespace osu.Game.Modes.Osu.Objects
|
||||
{
|
||||
public class Slider : OsuHitObject
|
||||
{
|
||||
public override double EndTime => StartTime + (RepeatCount + 1) * Curve.Length;
|
||||
public override double EndTime => StartTime + RepeatCount * Curve.Length / Velocity;
|
||||
|
||||
public double Velocity;
|
||||
|
||||
public override void SetDefaultsFromBeatmap(Beatmap beatmap)
|
||||
{
|
||||
Velocity = 100 / beatmap.BeatLengthAt(StartTime, true) * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
|
||||
}
|
||||
|
||||
public int RepeatCount;
|
||||
|
||||
public SliderCurve Curve;
|
||||
|
||||
}
|
||||
|
||||
public class SliderCurve
|
||||
@ -42,11 +51,14 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
|
||||
public Vector2 PositionAt(double progress)
|
||||
{
|
||||
int index = (int)(progress * (calculatedPath.Count - 1));
|
||||
progress = MathHelper.Clamp(progress, 0, 1);
|
||||
|
||||
Vector2 pos = calculatedPath[index];
|
||||
if (index != progress)
|
||||
pos += (calculatedPath[index + 1] - pos) * (float)(progress - index);
|
||||
double index = progress * (calculatedPath.Count - 1);
|
||||
int flooredIndex = (int)index;
|
||||
|
||||
Vector2 pos = calculatedPath[flooredIndex];
|
||||
if (index != flooredIndex)
|
||||
pos += (calculatedPath[flooredIndex + 1] - pos) * (float)(index - flooredIndex);
|
||||
|
||||
return pos;
|
||||
}
|
||||
@ -201,5 +213,5 @@ namespace osu.Game.Modes.Osu.Objects
|
||||
Bezier,
|
||||
Linear,
|
||||
PerfectCurve
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -16,5 +16,27 @@ namespace osu.Game.Beatmaps
|
||||
public List<HitObject> HitObjects { get; set; }
|
||||
public List<ControlPoint> ControlPoints { get; set; }
|
||||
public List<Color4> ComboColors { get; set; }
|
||||
|
||||
public double BeatLengthAt(double time, bool applyMultipliers = false)
|
||||
{
|
||||
int point = 0;
|
||||
int samplePoint = 0;
|
||||
|
||||
for (int i = 0; i < ControlPoints.Count; i++)
|
||||
if (ControlPoints[i].Time <= time)
|
||||
{
|
||||
if (ControlPoints[i].TimingChange)
|
||||
point = i;
|
||||
else
|
||||
samplePoint = i;
|
||||
}
|
||||
|
||||
double mult = 1;
|
||||
|
||||
if (applyMultipliers && samplePoint > point && ControlPoints[samplePoint].BeatLength < 0)
|
||||
mult = ControlPoints[samplePoint].VelocityAdjustment;
|
||||
|
||||
return ControlPoints[point].BeatLength * mult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +187,25 @@ namespace osu.Game.Beatmaps.Formats
|
||||
|
||||
private void handleTimingPoints(Beatmap beatmap, string val)
|
||||
{
|
||||
// TODO
|
||||
ControlPoint cp = null;
|
||||
|
||||
string[] split = val.Split(',');
|
||||
|
||||
if (split.Length > 2)
|
||||
{
|
||||
int kiai_flags = split.Length > 7 ? Convert.ToInt32(split[7], NumberFormatInfo.InvariantInfo) : 0;
|
||||
double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo);
|
||||
cp = new ControlPoint
|
||||
{
|
||||
Time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo),
|
||||
BeatLength = beatLength > 0 ? beatLength : 0,
|
||||
VelocityAdjustment = beatLength < 0 ? -beatLength / 100.0 : 1,
|
||||
TimingChange = split.Length <= 6 || split[6][0] == '1',
|
||||
};
|
||||
}
|
||||
|
||||
if (cp != null)
|
||||
beatmap.ControlPoints.Add(cp);
|
||||
}
|
||||
|
||||
private void handleColours(Beatmap beatmap, string key, string val)
|
||||
@ -275,8 +293,12 @@ namespace osu.Game.Beatmaps.Formats
|
||||
break;
|
||||
case Section.HitObjects:
|
||||
var obj = parser?.Parse(val);
|
||||
|
||||
if (obj != null)
|
||||
{
|
||||
obj.SetDefaultsFromBeatmap(beatmap);
|
||||
beatmap.HitObjects.Add(obj);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -12,5 +12,14 @@ namespace osu.Game.Beatmaps.Timing
|
||||
public class ControlPoint
|
||||
{
|
||||
public double Time;
|
||||
public double BeatLength;
|
||||
public double VelocityAdjustment;
|
||||
public bool TimingChange;
|
||||
}
|
||||
|
||||
internal enum TimeSignatures
|
||||
{
|
||||
SimpleQuadruple = 4,
|
||||
SimpleTriple = 3
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
//Copyright (c) 2007-2016 ppy Pty Ltd <contact@ppy.sh>.
|
||||
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Samples;
|
||||
using OpenTK.Graphics;
|
||||
|
||||
@ -21,5 +22,7 @@ namespace osu.Game.Modes.Objects
|
||||
public double Duration => EndTime - StartTime;
|
||||
|
||||
public HitSampleInfo Sample;
|
||||
|
||||
public virtual void SetDefaultsFromBeatmap(Beatmap beatmap) { }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user