mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:28:00 +08:00
Refactor spinner SPM counter for skinning purposes
This commit is contained in:
parent
f49481e308
commit
0bf84e473d
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
|
|
||||||
private void runSpmTest(Mod mod)
|
private void runSpmTest(Mod mod)
|
||||||
{
|
{
|
||||||
SpinnerSpmCounter spmCounter = null;
|
SpinnerSpmCalculator spmCalculator = null;
|
||||||
|
|
||||||
CreateModTest(new ModTestData
|
CreateModTest(new ModTestData
|
||||||
{
|
{
|
||||||
@ -53,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("fetch SPM counter", () =>
|
AddUntilStep("fetch SPM calculator", () =>
|
||||||
{
|
{
|
||||||
spmCounter = this.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
spmCalculator = this.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||||
return spmCounter != null;
|
return spmCalculator != null;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5));
|
AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCalculator.Result.Value, 477, 5));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
|||||||
Beatmap = singleSpinnerBeatmap,
|
Beatmap = singleSpinnerBeatmap,
|
||||||
PassCondition = () =>
|
PassCondition = () =>
|
||||||
{
|
{
|
||||||
var counter = Player.ChildrenOfType<SpinnerSpmCounter>().SingleOrDefault();
|
var counter = Player.ChildrenOfType<SpinnerSpmCalculator>().SingleOrDefault();
|
||||||
return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1);
|
return counter != null && Precision.AlmostEquals(counter.Result.Value, 286, 1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -168,13 +168,13 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
double estimatedSpm = 0;
|
double estimatedSpm = 0;
|
||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
|
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpinsPerMinute.Value);
|
||||||
|
|
||||||
addSeekStep(2000);
|
addSeekStep(2000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
|
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpinsPerMinute.Value, estimatedSpm, 1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCase(0.5)]
|
[TestCase(0.5)]
|
||||||
@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("retrieve spinner state", () =>
|
AddStep("retrieve spinner state", () =>
|
||||||
{
|
{
|
||||||
expectedProgress = drawableSpinner.Progress;
|
expectedProgress = drawableSpinner.Progress;
|
||||||
expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute;
|
expectedSpm = drawableSpinner.SpinsPerMinute.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
addSeekStep(0);
|
addSeekStep(0);
|
||||||
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
addSeekStep(1000);
|
addSeekStep(1000);
|
||||||
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05));
|
||||||
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0));
|
AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpinsPerMinute.Value, 2.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay
|
||||||
|
@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result;
|
||||||
|
|
||||||
public SpinnerRotationTracker RotationTracker { get; private set; }
|
public SpinnerRotationTracker RotationTracker { get; private set; }
|
||||||
public SpinnerSpmCounter SpmCounter { get; private set; }
|
|
||||||
|
private SpinnerSpmCalculator spmCalculator;
|
||||||
|
|
||||||
private Container<DrawableSpinnerTick> ticks;
|
private Container<DrawableSpinnerTick> ticks;
|
||||||
private PausableSkinnableSound spinningSample;
|
private PausableSkinnableSound spinningSample;
|
||||||
@ -43,7 +44,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindable<double> GainedBonus => gainedBonus;
|
public IBindable<double> GainedBonus => gainedBonus;
|
||||||
|
|
||||||
private readonly Bindable<double> gainedBonus = new Bindable<double>();
|
private readonly Bindable<double> gainedBonus = new BindableDouble();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of spins per minute this spinner is spinning at, for display purposes.
|
||||||
|
/// </summary>
|
||||||
|
public readonly IBindable<double> SpinsPerMinute = new BindableDouble();
|
||||||
|
|
||||||
private const double fade_out_duration = 160;
|
private const double fade_out_duration = 160;
|
||||||
|
|
||||||
@ -63,7 +69,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
Origin = Anchor.Centre;
|
Origin = Anchor.Centre;
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
AddInternal(spmCalculator = new SpinnerSpmCalculator
|
||||||
|
{
|
||||||
|
Result = { BindTarget = SpinsPerMinute },
|
||||||
|
});
|
||||||
|
|
||||||
|
AddRangeInternal(new Drawable[]
|
||||||
{
|
{
|
||||||
ticks = new Container<DrawableSpinnerTick>(),
|
ticks = new Container<DrawableSpinnerTick>(),
|
||||||
new AspectContainer
|
new AspectContainer
|
||||||
@ -77,20 +88,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
RotationTracker = new SpinnerRotationTracker(this)
|
RotationTracker = new SpinnerRotationTracker(this)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
SpmCounter = new SpinnerSpmCounter
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Y = 120,
|
|
||||||
Alpha = 0
|
|
||||||
},
|
|
||||||
spinningSample = new PausableSkinnableSound
|
spinningSample = new PausableSkinnableSound
|
||||||
{
|
{
|
||||||
Volume = { Value = 0 },
|
Volume = { Value = 0 },
|
||||||
Looping = true,
|
Looping = true,
|
||||||
Frequency = { Value = spinning_sample_initial_frequency }
|
Frequency = { Value = spinning_sample_initial_frequency }
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
PositionBindable.BindValueChanged(pos => Position = pos.NewValue);
|
||||||
}
|
}
|
||||||
@ -161,17 +165,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void UpdateStartTimeStateTransforms()
|
|
||||||
{
|
|
||||||
base.UpdateStartTimeStateTransforms();
|
|
||||||
|
|
||||||
if (Result?.TimeStarted is double startTime)
|
|
||||||
{
|
|
||||||
using (BeginAbsoluteSequence(startTime))
|
|
||||||
fadeInCounter();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void UpdateHitStateTransforms(ArmedState state)
|
protected override void UpdateHitStateTransforms(ArmedState state)
|
||||||
{
|
{
|
||||||
base.UpdateHitStateTransforms(state);
|
base.UpdateHitStateTransforms(state);
|
||||||
@ -282,22 +275,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
|||||||
{
|
{
|
||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
if (!SpmCounter.IsPresent && RotationTracker.Tracking)
|
if (Result.TimeStarted == null && RotationTracker.Tracking)
|
||||||
{
|
Result.TimeStarted = Time.Current;
|
||||||
Result.TimeStarted ??= Time.Current;
|
|
||||||
fadeInCounter();
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't update after end time to avoid the rate display dropping during fade out.
|
// don't update after end time to avoid the rate display dropping during fade out.
|
||||||
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
// this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period.
|
||||||
if (Time.Current <= HitObject.EndTime)
|
if (Time.Current <= HitObject.EndTime)
|
||||||
SpmCounter.SetRotation(Result.RateAdjustedRotation);
|
spmCalculator.SetRotation(Result.RateAdjustedRotation);
|
||||||
|
|
||||||
updateBonusScore();
|
updateBonusScore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn);
|
|
||||||
|
|
||||||
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
|
||||||
|
|
||||||
private int wholeSpins;
|
private int wholeSpins;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
private OsuSpriteText bonusCounter;
|
private OsuSpriteText bonusCounter;
|
||||||
|
|
||||||
|
private Container spmContainer;
|
||||||
|
private OsuSpriteText spmCounter;
|
||||||
|
|
||||||
public DefaultSpinner()
|
public DefaultSpinner()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -46,11 +50,37 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Font = OsuFont.Numeric.With(size: 24),
|
Font = OsuFont.Numeric.With(size: 24),
|
||||||
Y = -120,
|
Y = -120,
|
||||||
|
},
|
||||||
|
spmContainer = new Container
|
||||||
|
{
|
||||||
|
Alpha = 0f,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Y = 120,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
spmCounter = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Text = @"0",
|
||||||
|
Font = OsuFont.Numeric.With(size: 24)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Text = @"SPINS PER MINUTE",
|
||||||
|
Font = OsuFont.Numeric.With(size: 12),
|
||||||
|
Y = 30
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private IBindable<double> gainedBonus;
|
private IBindable<double> gainedBonus;
|
||||||
|
private IBindable<double> spinsPerMinute;
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
@ -63,6 +93,40 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
bonusCounter.FadeOutFromOne(1500);
|
bonusCounter.FadeOutFromOne(1500);
|
||||||
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
spinsPerMinute = drawableSpinner.SpinsPerMinute.GetBoundCopy();
|
||||||
|
spinsPerMinute.BindValueChanged(spm =>
|
||||||
|
{
|
||||||
|
spmCounter.Text = Math.Truncate(spm.NewValue).ToString(@"#0");
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
if (!spmContainer.IsPresent && drawableSpinner.Result?.TimeStarted != null)
|
||||||
|
fadeCounterOnTimeStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state)
|
||||||
|
{
|
||||||
|
if (!(drawableHitObject is DrawableSpinner))
|
||||||
|
return;
|
||||||
|
|
||||||
|
fadeCounterOnTimeStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fadeCounterOnTimeStart()
|
||||||
|
{
|
||||||
|
if (drawableSpinner.Result?.TimeStarted is double startTime)
|
||||||
|
{
|
||||||
|
using (BeginAbsoluteSequence(startTime))
|
||||||
|
spmContainer.FadeIn(drawableSpinner.HitObject.TimeFadeIn);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,77 +1,37 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Graphics;
|
|
||||||
using osu.Game.Graphics.Sprites;
|
|
||||||
using osu.Game.Rulesets.Objects.Drawables;
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||||
{
|
{
|
||||||
public class SpinnerSpmCounter : Container
|
public class SpinnerSpmCalculator : Component
|
||||||
{
|
{
|
||||||
|
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
||||||
|
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The resultant spins per minute value, which is updated via <see cref="SetRotation"/>.
|
||||||
|
/// </summary>
|
||||||
|
public IBindable<double> Result => result;
|
||||||
|
|
||||||
|
private readonly Bindable<double> result = new BindableDouble();
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private DrawableHitObject drawableSpinner { get; set; }
|
private DrawableHitObject drawableSpinner { get; set; }
|
||||||
|
|
||||||
private readonly OsuSpriteText spmText;
|
|
||||||
|
|
||||||
public SpinnerSpmCounter()
|
|
||||||
{
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
spmText = new OsuSpriteText
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Text = @"0",
|
|
||||||
Font = OsuFont.Numeric.With(size: 24)
|
|
||||||
},
|
|
||||||
new OsuSpriteText
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopCentre,
|
|
||||||
Origin = Anchor.TopCentre,
|
|
||||||
Text = @"SPINS PER MINUTE",
|
|
||||||
Font = OsuFont.Numeric.With(size: 12),
|
|
||||||
Y = 30
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
drawableSpinner.HitObjectApplied += resetState;
|
drawableSpinner.HitObjectApplied += resetState;
|
||||||
}
|
}
|
||||||
|
|
||||||
private double spm;
|
|
||||||
|
|
||||||
public double SpinsPerMinute
|
|
||||||
{
|
|
||||||
get => spm;
|
|
||||||
private set
|
|
||||||
{
|
|
||||||
if (value == spm) return;
|
|
||||||
|
|
||||||
spm = value;
|
|
||||||
spmText.Text = Math.Truncate(value).ToString(@"#0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private struct RotationRecord
|
|
||||||
{
|
|
||||||
public float Rotation;
|
|
||||||
public double Time;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Queue<RotationRecord> records = new Queue<RotationRecord>();
|
|
||||||
private const double spm_count_duration = 595; // not using hundreds to avoid frame rounding issues
|
|
||||||
|
|
||||||
public void SetRotation(float currentRotation)
|
public void SetRotation(float currentRotation)
|
||||||
{
|
{
|
||||||
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
// Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result.
|
||||||
@ -88,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
|
while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration)
|
||||||
record = records.Dequeue();
|
record = records.Dequeue();
|
||||||
|
|
||||||
SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
result.Value = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360;
|
||||||
}
|
}
|
||||||
|
|
||||||
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current });
|
||||||
@ -96,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
|
|
||||||
private void resetState(DrawableHitObject hitObject)
|
private void resetState(DrawableHitObject hitObject)
|
||||||
{
|
{
|
||||||
SpinsPerMinute = 0;
|
result.Value = 0;
|
||||||
records.Clear();
|
records.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,5 +67,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
if (drawableSpinner != null)
|
if (drawableSpinner != null)
|
||||||
drawableSpinner.HitObjectApplied -= resetState;
|
drawableSpinner.HitObjectApplied -= resetState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct RotationRecord
|
||||||
|
{
|
||||||
|
public float Rotation;
|
||||||
|
public double Time;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user