mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 17:32:54 +08:00
Merge branch 'master' into realm-stable-subscriptions
This commit is contained in:
commit
9ff9611296
@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
|
|||||||
if (!effectPoint.KiaiMode)
|
if (!effectPoint.KiaiMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (beatIndex % (int)timingPoint.TimeSignature != 0)
|
if (beatIndex % timingPoint.TimeSignature.Numerator != 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
double duration = timingPoint.BeatLength * 2;
|
double duration = timingPoint.BeatLength * 2;
|
||||||
|
@ -178,17 +178,17 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
var timingPoint = controlPoints.TimingPointAt(0);
|
var timingPoint = controlPoints.TimingPointAt(0);
|
||||||
Assert.AreEqual(956, timingPoint.Time);
|
Assert.AreEqual(956, timingPoint.Time);
|
||||||
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
|
Assert.AreEqual(329.67032967033, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
timingPoint = controlPoints.TimingPointAt(48428);
|
timingPoint = controlPoints.TimingPointAt(48428);
|
||||||
Assert.AreEqual(956, timingPoint.Time);
|
Assert.AreEqual(956, timingPoint.Time);
|
||||||
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
|
Assert.AreEqual(329.67032967033d, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
timingPoint = controlPoints.TimingPointAt(119637);
|
timingPoint = controlPoints.TimingPointAt(119637);
|
||||||
Assert.AreEqual(119637, timingPoint.Time);
|
Assert.AreEqual(119637, timingPoint.Time);
|
||||||
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
|
Assert.AreEqual(659.340659340659, timingPoint.BeatLength);
|
||||||
Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature);
|
Assert.AreEqual(TimeSignature.SimpleQuadruple, timingPoint.TimeSignature);
|
||||||
|
|
||||||
var difficultyPoint = controlPoints.DifficultyPointAt(0);
|
var difficultyPoint = controlPoints.DifficultyPointAt(0);
|
||||||
Assert.AreEqual(0, difficultyPoint.Time);
|
Assert.AreEqual(0, difficultyPoint.Time);
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
|
|
||||||
const int beat_length_numerator = 2000;
|
const int beat_length_numerator = 2000;
|
||||||
const int beat_length_denominator = 7;
|
const int beat_length_denominator = 7;
|
||||||
const TimeSignatures signature = TimeSignatures.SimpleQuadruple;
|
TimeSignature signature = TimeSignature.SimpleQuadruple;
|
||||||
|
|
||||||
var beatmap = new Beatmap
|
var beatmap = new Beatmap
|
||||||
{
|
{
|
||||||
@ -49,7 +49,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
|
for (int i = 0; i * beat_length_denominator < barLines.Count; i++)
|
||||||
{
|
{
|
||||||
var barLine = barLines[i * beat_length_denominator];
|
var barLine = barLines[i * beat_length_denominator];
|
||||||
int expectedTime = beat_length_numerator * (int)signature * i;
|
int expectedTime = beat_length_numerator * signature.Numerator * i;
|
||||||
|
|
||||||
// every seventh bar's start time should be at least greater than the whole number we expect.
|
// every seventh bar's start time should be at least greater than the whole number we expect.
|
||||||
// It cannot be less, as that can affect overlapping scroll algorithms
|
// It cannot be less, as that can affect overlapping scroll algorithms
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
|
Assert.IsTrue(Precision.AlmostEquals(barLine.StartTime, expectedTime));
|
||||||
|
|
||||||
// check major/minor lines for good measure too
|
// check major/minor lines for good measure too
|
||||||
Assert.AreEqual(i % (int)signature == 0, barLine.Major);
|
Assert.AreEqual(i % signature.Numerator == 0, barLine.Major);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
|
editorBeatmap.BeatmapInfo.Metadata.Artist = "artist";
|
||||||
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
|
editorBeatmap.BeatmapInfo.Metadata.Title = "title";
|
||||||
});
|
});
|
||||||
|
AddStep("Set author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username = "author");
|
||||||
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
|
AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName = "difficulty");
|
||||||
|
|
||||||
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
|
||||||
@ -64,6 +65,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
|
AddStep("Save", () => InputManager.Keys(PlatformAction.Save));
|
||||||
|
|
||||||
checkMutations();
|
checkMutations();
|
||||||
|
AddAssert("Beatmap has correct .osu file path", () => editorBeatmap.BeatmapInfo.Path == "artist - title (author) [difficulty].osu");
|
||||||
|
|
||||||
AddStep("Exit", () => InputManager.Key(Key.Escape));
|
AddStep("Exit", () => InputManager.Key(Key.Escape));
|
||||||
|
|
||||||
@ -88,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
|
AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1);
|
||||||
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
|
AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7);
|
||||||
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
|
AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title");
|
||||||
|
AddAssert("Beatmap has correct author", () => editorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
|
||||||
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Screens.Edit.Timing;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneLabelledTimeSignature : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private LabelledTimeSignature timeSignature;
|
||||||
|
|
||||||
|
private void createLabelledTimeSignature(TimeSignature initial) => AddStep("create labelled time signature", () =>
|
||||||
|
{
|
||||||
|
Child = timeSignature = new LabelledTimeSignature
|
||||||
|
{
|
||||||
|
Label = "Time Signature",
|
||||||
|
RelativeSizeAxes = Axes.None,
|
||||||
|
Width = 400,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Current = { Value = initial }
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
private OsuTextBox numeratorTextBox => timeSignature.ChildrenOfType<OsuTextBox>().Single();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInitialValue()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleTriple);
|
||||||
|
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeViaCurrent()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("set current to 5/4", () => timeSignature.Current.Value = new TimeSignature(5));
|
||||||
|
|
||||||
|
AddAssert("current is 5/4", () => timeSignature.Current.Value.Equals(new TimeSignature(5)));
|
||||||
|
AddAssert("numerator is 5", () => numeratorTextBox.Current.Value == "5");
|
||||||
|
|
||||||
|
AddStep("set current to 3/4", () => timeSignature.Current.Value = TimeSignature.SimpleTriple);
|
||||||
|
|
||||||
|
AddAssert("current is 3/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleTriple));
|
||||||
|
AddAssert("numerator is 3", () => numeratorTextBox.Current.Value == "3");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestChangeNumerator()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
|
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||||
|
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestInvalidChangeRollbackOnCommit()
|
||||||
|
{
|
||||||
|
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
|
||||||
|
|
||||||
|
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
|
||||||
|
AddStep("drop focus", () => InputManager.ChangeFocus(null));
|
||||||
|
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
|
||||||
|
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
Add(new ModNightcore<HitObject>.NightcoreBeatContainer());
|
Add(new ModNightcore<HitObject>.NightcoreBeatContainer());
|
||||||
|
|
||||||
AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple));
|
AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleQuadruple));
|
||||||
AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple));
|
AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignature.SimpleTriple));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays.Login;
|
using osu.Game.Overlays.Login;
|
||||||
|
using osu.Game.Users.Drawables;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Menus
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
{
|
{
|
||||||
@ -15,6 +17,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
public class TestSceneLoginPanel : OsuManualInputManagerTestScene
|
public class TestSceneLoginPanel : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
private LoginPanel loginPanel;
|
private LoginPanel loginPanel;
|
||||||
|
private int hideCount;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
@ -26,6 +29,7 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Width = 0.5f,
|
Width = 0.5f,
|
||||||
|
RequestHide = () => hideCount++,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -51,5 +55,22 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClickingOnFlagClosesPanel()
|
||||||
|
{
|
||||||
|
AddStep("reset hide count", () => hideCount = 0);
|
||||||
|
|
||||||
|
AddStep("logout", () => API.Logout());
|
||||||
|
AddStep("enter password", () => loginPanel.ChildrenOfType<OsuPasswordTextBox>().First().Text = "password");
|
||||||
|
AddStep("submit", () => loginPanel.ChildrenOfType<OsuButton>().First(b => b.Text.ToString() == "Sign in").TriggerClick());
|
||||||
|
|
||||||
|
AddStep("click on flag", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(loginPanel.ChildrenOfType<UpdateableFlag>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
AddAssert("hide requested", () => hideCount == 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Components
|
|||||||
|
|
||||||
if (manager == null)
|
if (manager == null)
|
||||||
{
|
{
|
||||||
AddInternal(manager = new ChannelManager());
|
AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } });
|
||||||
Channel.BindTo(manager.CurrentChannel);
|
Channel.BindTo(manager.CurrentChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Beatmaps
|
|||||||
private static string getFilename(BeatmapInfo beatmapInfo)
|
private static string getFilename(BeatmapInfo beatmapInfo)
|
||||||
{
|
{
|
||||||
var metadata = beatmapInfo.Metadata;
|
var metadata = beatmapInfo.Metadata;
|
||||||
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidArchiveContentFilename();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time signature at this control point.
|
/// The time signature at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
|
public readonly Bindable<TimeSignature> TimeSignatureBindable = new Bindable<TimeSignature>(TimeSignature.SimpleQuadruple);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
|
||||||
@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time signature at this control point.
|
/// The time signature at this control point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TimeSignatures TimeSignature
|
public TimeSignature TimeSignature
|
||||||
{
|
{
|
||||||
get => TimeSignatureBindable.Value;
|
get => TimeSignatureBindable.Value;
|
||||||
set => TimeSignatureBindable.Value = value;
|
set => TimeSignatureBindable.Value = value;
|
||||||
|
@ -340,9 +340,9 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
double beatLength = Parsing.ParseDouble(split[1].Trim());
|
double beatLength = Parsing.ParseDouble(split[1].Trim());
|
||||||
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
|
double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1;
|
||||||
|
|
||||||
TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple;
|
TimeSignature timeSignature = TimeSignature.SimpleQuadruple;
|
||||||
if (split.Length >= 3)
|
if (split.Length >= 3)
|
||||||
timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)Parsing.ParseInt(split[2]);
|
timeSignature = split[2][0] == '0' ? TimeSignature.SimpleQuadruple : new TimeSignature(Parsing.ParseInt(split[2]));
|
||||||
|
|
||||||
LegacySampleBank sampleSet = defaultSampleBank;
|
LegacySampleBank sampleSet = defaultSampleBank;
|
||||||
if (split.Length >= 4)
|
if (split.Length >= 4)
|
||||||
|
@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
if (effectPoint.OmitFirstBarLine)
|
if (effectPoint.OmitFirstBarLine)
|
||||||
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
|
effectFlags |= LegacyEffectFlags.OmitFirstBarLine;
|
||||||
|
|
||||||
writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},"));
|
writer.Write(FormattableString.Invariant($"{legacyControlPoints.TimingPointAt(time).TimeSignature.Numerator},"));
|
||||||
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
|
writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
|
writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
|
writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},"));
|
||||||
|
45
osu.Game/Beatmaps/Timing/TimeSignature.cs
Normal file
45
osu.Game/Beatmaps/Timing/TimeSignature.cs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace osu.Game.Beatmaps.Timing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the time signature of a track.
|
||||||
|
/// For now, the lower numeral can only be 4; support for other denominators can be considered at a later date.
|
||||||
|
/// </summary>
|
||||||
|
public class TimeSignature : IEquatable<TimeSignature>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The numerator of a signature.
|
||||||
|
/// </summary>
|
||||||
|
public int Numerator { get; }
|
||||||
|
|
||||||
|
// TODO: support time signatures with a denominator other than 4
|
||||||
|
// this in particular requires a new beatmap format.
|
||||||
|
|
||||||
|
public TimeSignature(int numerator)
|
||||||
|
{
|
||||||
|
if (numerator < 1)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(numerator), numerator, "The numerator of a time signature must be positive.");
|
||||||
|
|
||||||
|
Numerator = numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TimeSignature SimpleTriple { get; } = new TimeSignature(3);
|
||||||
|
public static TimeSignature SimpleQuadruple { get; } = new TimeSignature(4);
|
||||||
|
|
||||||
|
public override string ToString() => $"{Numerator}/4";
|
||||||
|
|
||||||
|
public bool Equals(TimeSignature other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
|
||||||
|
return Numerator == other.Numerator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode() => Numerator;
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,13 @@
|
|||||||
// 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.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.Timing
|
namespace osu.Game.Beatmaps.Timing
|
||||||
{
|
{
|
||||||
public enum TimeSignatures
|
[Obsolete("Use osu.Game.Beatmaps.Timing.TimeSignature instead.")]
|
||||||
|
public enum TimeSignatures // can be removed 20220722
|
||||||
{
|
{
|
||||||
[Description("4/4")]
|
[Description("4/4")]
|
||||||
SimpleQuadruple = 4,
|
SimpleQuadruple = 4,
|
||||||
|
@ -5,7 +5,6 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.EntityFrameworkCore.Storage;
|
using Microsoft.EntityFrameworkCore.Storage;
|
||||||
using osu.Framework.Development;
|
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Statistics;
|
using osu.Framework.Statistics;
|
||||||
@ -151,9 +150,6 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
|
Logger.Log($"Creating full EF database backup at {backupFilename}", LoggingTarget.Database);
|
||||||
|
|
||||||
if (DebugUtils.IsDebugBuild)
|
|
||||||
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
|
||||||
|
|
||||||
using (var source = storage.GetStream(DATABASE_NAME))
|
using (var source = storage.GetStream(DATABASE_NAME))
|
||||||
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
||||||
source.CopyTo(destination);
|
source.CopyTo(destination);
|
||||||
|
@ -1,55 +1,127 @@
|
|||||||
// 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.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Development;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osuTK;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
internal class EFToRealmMigrator
|
internal class EFToRealmMigrator : CompositeDrawable
|
||||||
{
|
{
|
||||||
private readonly DatabaseContextFactory efContextFactory;
|
public bool FinishedMigrating { get; private set; }
|
||||||
private readonly RealmContextFactory realmContextFactory;
|
|
||||||
private readonly OsuConfigManager config;
|
|
||||||
private readonly Storage storage;
|
|
||||||
|
|
||||||
public EFToRealmMigrator(DatabaseContextFactory efContextFactory, RealmContextFactory realmContextFactory, OsuConfigManager config, Storage storage)
|
[Resolved]
|
||||||
|
private DatabaseContextFactory efContextFactory { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RealmContextFactory realmContextFactory { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
private readonly OsuSpriteText currentOperationText;
|
||||||
|
|
||||||
|
public EFToRealmMigrator()
|
||||||
{
|
{
|
||||||
this.efContextFactory = efContextFactory;
|
RelativeSizeAxes = Axes.Both;
|
||||||
this.realmContextFactory = realmContextFactory;
|
|
||||||
this.config = config;
|
InternalChildren = new Drawable[]
|
||||||
this.storage = storage;
|
{
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = "Database migration in progress",
|
||||||
|
Font = OsuFont.Default.With(size: 40)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = "This could take a few minutes depending on the speed of your disk(s).",
|
||||||
|
Font = OsuFont.Default.With(size: 30)
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Text = "Please keep the window open until this completes!",
|
||||||
|
Font = OsuFont.Default.With(size: 30)
|
||||||
|
},
|
||||||
|
new LoadingSpinner(true)
|
||||||
|
{
|
||||||
|
State = { Value = Visibility.Visible }
|
||||||
|
},
|
||||||
|
currentOperationText = new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Font = OsuFont.Default.With(size: 30)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Run()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
createBackup();
|
base.LoadComplete();
|
||||||
|
|
||||||
using (var ef = efContextFactory.Get())
|
Task.Factory.StartNew(() =>
|
||||||
{
|
{
|
||||||
migrateSettings(ef);
|
using (var ef = efContextFactory.Get())
|
||||||
migrateSkins(ef);
|
{
|
||||||
migrateBeatmaps(ef);
|
migrateSettings(ef);
|
||||||
migrateScores(ef);
|
migrateSkins(ef);
|
||||||
}
|
migrateBeatmaps(ef);
|
||||||
|
migrateScores(ef);
|
||||||
|
}
|
||||||
|
|
||||||
// Delete the database permanently.
|
// Delete the database permanently.
|
||||||
// Will cause future startups to not attempt migration.
|
// Will cause future startups to not attempt migration.
|
||||||
Logger.Log("Migration successful, deleting EF database", LoggingTarget.Database);
|
log("Migration successful, deleting EF database");
|
||||||
efContextFactory.ResetDatabase();
|
efContextFactory.ResetDatabase();
|
||||||
|
|
||||||
|
if (DebugUtils.IsDebugBuild)
|
||||||
|
Logger.Log("Your development database has been fully migrated to realm. If you switch back to a pre-realm branch and need your previous database, rename the backup file back to \"client.db\".\n\nNote that doing this can potentially leave your file store in a bad state.", level: LogLevel.Important);
|
||||||
|
}, TaskCreationOptions.LongRunning).ContinueWith(t =>
|
||||||
|
{
|
||||||
|
FinishedMigrating = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void log(string message)
|
||||||
|
{
|
||||||
|
Logger.Log(message, LoggingTarget.Database);
|
||||||
|
Scheduler.AddOnce(m => currentOperationText.Text = m, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void migrateBeatmaps(OsuDbContext ef)
|
private void migrateBeatmaps(OsuDbContext ef)
|
||||||
@ -62,12 +134,12 @@ namespace osu.Game.Database
|
|||||||
.Include(s => s.Files).ThenInclude(f => f.FileInfo)
|
.Include(s => s.Files).ThenInclude(f => f.FileInfo)
|
||||||
.Include(s => s.Metadata);
|
.Include(s => s.Metadata);
|
||||||
|
|
||||||
Logger.Log("Beginning beatmaps migration to realm", LoggingTarget.Database);
|
log("Beginning beatmaps migration to realm");
|
||||||
|
|
||||||
// previous entries in EF are removed post migration.
|
// previous entries in EF are removed post migration.
|
||||||
if (!existingBeatmapSets.Any())
|
if (!existingBeatmapSets.Any())
|
||||||
{
|
{
|
||||||
Logger.Log("No beatmaps found to migrate", LoggingTarget.Database);
|
log("No beatmaps found to migrate");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,13 +147,13 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
realmContextFactory.Run(realm =>
|
realmContextFactory.Run(realm =>
|
||||||
{
|
{
|
||||||
Logger.Log($"Found {count} beatmaps in EF", LoggingTarget.Database);
|
log($"Found {count} beatmaps in EF");
|
||||||
|
|
||||||
// only migrate data if the realm database is empty.
|
// only migrate data if the realm database is empty.
|
||||||
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
// note that this cannot be written as: `realm.All<BeatmapSetInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||||
if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
|
if (realm.All<BeatmapSetInfo>().Any(s => !s.Protected))
|
||||||
{
|
{
|
||||||
Logger.Log("Skipping migration as realm already has beatmaps loaded", LoggingTarget.Database);
|
log("Skipping migration as realm already has beatmaps loaded");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -96,7 +168,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
transaction = realm.BeginWrite();
|
transaction = realm.BeginWrite();
|
||||||
Logger.Log($"Migrated {written}/{count} beatmaps...", LoggingTarget.Database);
|
log($"Migrated {written}/{count} beatmaps...");
|
||||||
}
|
}
|
||||||
|
|
||||||
var realmBeatmapSet = new BeatmapSetInfo
|
var realmBeatmapSet = new BeatmapSetInfo
|
||||||
@ -156,7 +228,7 @@ namespace osu.Game.Database
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Successfully migrated {count} beatmaps to realm", LoggingTarget.Database);
|
log($"Successfully migrated {count} beatmaps to realm");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -193,12 +265,12 @@ namespace osu.Game.Database
|
|||||||
.Include(s => s.Files)
|
.Include(s => s.Files)
|
||||||
.ThenInclude(f => f.FileInfo);
|
.ThenInclude(f => f.FileInfo);
|
||||||
|
|
||||||
Logger.Log("Beginning scores migration to realm", LoggingTarget.Database);
|
log("Beginning scores migration to realm");
|
||||||
|
|
||||||
// previous entries in EF are removed post migration.
|
// previous entries in EF are removed post migration.
|
||||||
if (!existingScores.Any())
|
if (!existingScores.Any())
|
||||||
{
|
{
|
||||||
Logger.Log("No scores found to migrate", LoggingTarget.Database);
|
log("No scores found to migrate");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,12 +278,12 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
realmContextFactory.Run(realm =>
|
realmContextFactory.Run(realm =>
|
||||||
{
|
{
|
||||||
Logger.Log($"Found {count} scores in EF", LoggingTarget.Database);
|
log($"Found {count} scores in EF");
|
||||||
|
|
||||||
// only migrate data if the realm database is empty.
|
// only migrate data if the realm database is empty.
|
||||||
if (realm.All<ScoreInfo>().Any())
|
if (realm.All<ScoreInfo>().Any())
|
||||||
{
|
{
|
||||||
Logger.Log("Skipping migration as realm already has scores loaded", LoggingTarget.Database);
|
log("Skipping migration as realm already has scores loaded");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -226,7 +298,7 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
transaction = realm.BeginWrite();
|
transaction = realm.BeginWrite();
|
||||||
Logger.Log($"Migrated {written}/{count} scores...", LoggingTarget.Database);
|
log($"Migrated {written}/{count} scores...");
|
||||||
}
|
}
|
||||||
|
|
||||||
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
|
var beatmap = realm.All<BeatmapInfo>().First(b => b.Hash == score.BeatmapInfo.Hash);
|
||||||
@ -270,7 +342,7 @@ namespace osu.Game.Database
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log($"Successfully migrated {count} scores to realm", LoggingTarget.Database);
|
log($"Successfully migrated {count} scores to realm");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -309,7 +381,7 @@ namespace osu.Game.Database
|
|||||||
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
// note that this cannot be written as: `realm.All<SkinInfo>().All(s => s.Protected)`, because realm does not support `.All()`.
|
||||||
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
if (!realm.All<SkinInfo>().Any(s => !s.Protected))
|
||||||
{
|
{
|
||||||
Logger.Log($"Migrating {existingSkins.Count} skins", LoggingTarget.Database);
|
log($"Migrating {existingSkins.Count} skins");
|
||||||
|
|
||||||
foreach (var skin in existingSkins)
|
foreach (var skin in existingSkins)
|
||||||
{
|
{
|
||||||
@ -358,7 +430,7 @@ namespace osu.Game.Database
|
|||||||
if (!existingSettings.Any())
|
if (!existingSettings.Any())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Logger.Log("Beginning settings migration to realm", LoggingTarget.Database);
|
log("Beginning settings migration to realm");
|
||||||
|
|
||||||
realmContextFactory.Run(realm =>
|
realmContextFactory.Run(realm =>
|
||||||
{
|
{
|
||||||
@ -367,7 +439,7 @@ namespace osu.Game.Database
|
|||||||
// only migrate data if the realm database is empty.
|
// only migrate data if the realm database is empty.
|
||||||
if (!realm.All<RealmRulesetSetting>().Any())
|
if (!realm.All<RealmRulesetSetting>().Any())
|
||||||
{
|
{
|
||||||
Logger.Log($"Migrating {existingSettings.Count} settings", LoggingTarget.Database);
|
log($"Migrating {existingSettings.Count} settings");
|
||||||
|
|
||||||
foreach (var dkb in existingSettings)
|
foreach (var dkb in existingSettings)
|
||||||
{
|
{
|
||||||
@ -396,17 +468,5 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
||||||
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
efContextFactory.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
||||||
|
|
||||||
private void createBackup()
|
|
||||||
{
|
|
||||||
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
|
||||||
|
|
||||||
efContextFactory.CreateBackup($"client.{migration}.db");
|
|
||||||
realmContextFactory.CreateBackup($"client.{migration}.realm");
|
|
||||||
|
|
||||||
using (var source = storage.GetStream("collection.db"))
|
|
||||||
using (var destination = storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
|
|
||||||
source.CopyTo(destination);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -161,6 +161,11 @@ namespace osu.Game
|
|||||||
|
|
||||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
|
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(global_track_volume_adjust);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A legacy EF context factory if migration has not been performed to realm yet.
|
||||||
|
/// </summary>
|
||||||
|
protected DatabaseContextFactory EFContextFactory { get; private set; }
|
||||||
|
|
||||||
public OsuGameBase()
|
public OsuGameBase()
|
||||||
{
|
{
|
||||||
UseDevelopmentServer = DebugUtils.IsDebugBuild;
|
UseDevelopmentServer = DebugUtils.IsDebugBuild;
|
||||||
@ -184,18 +189,28 @@ namespace osu.Game
|
|||||||
|
|
||||||
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
|
Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly));
|
||||||
|
|
||||||
DatabaseContextFactory efContextFactory = Storage.Exists(DatabaseContextFactory.DATABASE_NAME)
|
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
|
||||||
? new DatabaseContextFactory(Storage)
|
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
|
||||||
: null;
|
|
||||||
|
|
||||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", efContextFactory));
|
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client", EFContextFactory));
|
||||||
|
|
||||||
dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage));
|
dependencies.Cache(RulesetStore = new RulesetStore(realmFactory, Storage));
|
||||||
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
||||||
|
|
||||||
// A non-null context factory means there's still content to migrate.
|
// Backup is taken here rather than in EFToRealmMigrator to avoid recycling realm contexts
|
||||||
if (efContextFactory != null)
|
// after initial usages below. It can be moved once a direction is established for handling re-subscription.
|
||||||
new EFToRealmMigrator(efContextFactory, realmFactory, LocalConfig, Storage).Run();
|
// See https://github.com/ppy/osu/pull/16547 for more discussion.
|
||||||
|
if (EFContextFactory != null)
|
||||||
|
{
|
||||||
|
string migration = $"before_final_migration_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}";
|
||||||
|
|
||||||
|
EFContextFactory.CreateBackup($"client.{migration}.db");
|
||||||
|
realmFactory.CreateBackup($"client.{migration}.realm");
|
||||||
|
|
||||||
|
using (var source = Storage.GetStream("collection.db"))
|
||||||
|
using (var destination = Storage.GetStream($"collection.{migration}.db", FileAccess.Write, FileMode.CreateNew))
|
||||||
|
source.CopyTo(destination);
|
||||||
|
}
|
||||||
|
|
||||||
dependencies.CacheAs(Storage);
|
dependencies.CacheAs(Storage);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
if (!IsBeatSyncedWithTrack) return;
|
if (!IsBeatSyncedWithTrack) return;
|
||||||
|
|
||||||
int timeSignature = (int)timingPoint.TimeSignature;
|
int timeSignature = timingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
// play metronome from one measure before the first object.
|
// play metronome from one measure before the first object.
|
||||||
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
||||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
{
|
{
|
||||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||||
|
|
||||||
int beatsPerBar = (int)timingPoint.TimeSignature;
|
int beatsPerBar = timingPoint.TimeSignature.Numerator;
|
||||||
int segmentLength = beatsPerBar * Divisor * bars_per_segment;
|
int segmentLength = beatsPerBar * Divisor * bars_per_segment;
|
||||||
|
|
||||||
if (!IsBeatSyncedWithTrack)
|
if (!IsBeatSyncedWithTrack)
|
||||||
@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature);
|
playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void playBeatFor(int beatIndex, TimeSignatures signature)
|
private void playBeatFor(int beatIndex, TimeSignature signature)
|
||||||
{
|
{
|
||||||
if (beatIndex == 0)
|
if (beatIndex == 0)
|
||||||
finishSample?.Play();
|
finishSample?.Play();
|
||||||
|
|
||||||
switch (signature)
|
switch (signature.Numerator)
|
||||||
{
|
{
|
||||||
case TimeSignatures.SimpleTriple:
|
case 3:
|
||||||
switch (beatIndex % 6)
|
switch (beatIndex % 6)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
@ -127,7 +127,7 @@ namespace osu.Game.Rulesets.Mods
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case TimeSignatures.SimpleQuadruple:
|
case 4:
|
||||||
switch (beatIndex % 4)
|
switch (beatIndex % 4)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
|
@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
int currentBeat = 0;
|
int currentBeat = 0;
|
||||||
|
|
||||||
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
|
// Stop on the beat before the next timing point, or if there is no next timing point stop slightly past the last object
|
||||||
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature;
|
double endTime = i < timingPoints.Count - 1 ? timingPoints[i + 1].Time - currentTimingPoint.BeatLength : lastHitTime + currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
double barLength = currentTimingPoint.BeatLength * (int)currentTimingPoint.TimeSignature;
|
double barLength = currentTimingPoint.BeatLength * currentTimingPoint.TimeSignature.Numerator;
|
||||||
|
|
||||||
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
|
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
|
||||||
{
|
{
|
||||||
@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Objects
|
|||||||
BarLines.Add(new TBarLine
|
BarLines.Add(new TBarLine
|
||||||
{
|
{
|
||||||
StartTime = t,
|
StartTime = t,
|
||||||
Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0
|
Major = currentBeat % currentTimingPoint.TimeSignature.Numerator == 0
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,7 +125,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
if (beat == 0 && i == 0)
|
if (beat == 0 && i == 0)
|
||||||
nextMinTick = float.MinValue;
|
nextMinTick = float.MinValue;
|
||||||
|
|
||||||
int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
|
int indexInBar = beat % (point.TimeSignature.Numerator * beatDivisor.Value);
|
||||||
|
|
||||||
int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
int divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
|
||||||
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
|
||||||
|
97
osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs
Normal file
97
osu.Game/Screens/Edit/Timing/LabelledTimeSignature.cs
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Beatmaps.Timing;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Timing
|
||||||
|
{
|
||||||
|
public class LabelledTimeSignature : LabelledComponent<LabelledTimeSignature.TimeSignatureBox, TimeSignature>
|
||||||
|
{
|
||||||
|
public LabelledTimeSignature()
|
||||||
|
: base(false)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override TimeSignatureBox CreateComponent() => new TimeSignatureBox();
|
||||||
|
|
||||||
|
public class TimeSignatureBox : CompositeDrawable, IHasCurrentValue<TimeSignature>
|
||||||
|
{
|
||||||
|
private readonly BindableWithCurrent<TimeSignature> current = new BindableWithCurrent<TimeSignature>(TimeSignature.SimpleQuadruple);
|
||||||
|
|
||||||
|
public Bindable<TimeSignature> Current
|
||||||
|
{
|
||||||
|
get => current.Current;
|
||||||
|
set => current.Current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OsuNumberBox numeratorBox;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
InternalChild = new FillFlowContainer
|
||||||
|
{
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
numeratorBox = new OsuNumberBox
|
||||||
|
{
|
||||||
|
Width = 40,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
CornerRadius = CORNER_RADIUS,
|
||||||
|
CommitOnFocusLost = true
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Margin = new MarginPadding
|
||||||
|
{
|
||||||
|
Left = 5,
|
||||||
|
Right = CONTENT_PADDING_HORIZONTAL
|
||||||
|
},
|
||||||
|
Text = "/ 4",
|
||||||
|
Font = OsuFont.Default.With(size: 20)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
Current.BindValueChanged(_ => updateFromCurrent(), true);
|
||||||
|
numeratorBox.OnCommit += (_, __) => updateFromNumeratorBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFromCurrent()
|
||||||
|
{
|
||||||
|
numeratorBox.Current.Value = Current.Value.Numerator.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFromNumeratorBox()
|
||||||
|
{
|
||||||
|
if (int.TryParse(numeratorBox.Current.Value, out int numerator) && numerator > 0)
|
||||||
|
Current.Value = new TimeSignature(numerator);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// trigger `Current` change to restore the numerator box's text to a valid value.
|
||||||
|
Current.TriggerChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -13,7 +13,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes
|
|||||||
public class TimingRowAttribute : RowAttribute
|
public class TimingRowAttribute : RowAttribute
|
||||||
{
|
{
|
||||||
private readonly BindableNumber<double> beatLength;
|
private readonly BindableNumber<double> beatLength;
|
||||||
private readonly Bindable<TimeSignatures> timeSignature;
|
private readonly Bindable<TimeSignature> timeSignature;
|
||||||
private OsuSpriteText text;
|
private OsuSpriteText text;
|
||||||
|
|
||||||
public TimingRowAttribute(TimingControlPoint timing)
|
public TimingRowAttribute(TimingControlPoint timing)
|
||||||
|
@ -6,7 +6,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Timing;
|
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
internal class TimingSection : Section<TimingControlPoint>
|
internal class TimingSection : Section<TimingControlPoint>
|
||||||
{
|
{
|
||||||
private SettingsSlider<double> bpmSlider;
|
private SettingsSlider<double> bpmSlider;
|
||||||
private SettingsEnumDropdown<TimeSignatures> timeSignature;
|
private LabelledTimeSignature timeSignature;
|
||||||
private BPMTextBox bpmTextEntry;
|
private BPMTextBox bpmTextEntry;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -25,10 +24,10 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
{
|
{
|
||||||
bpmTextEntry = new BPMTextBox(),
|
bpmTextEntry = new BPMTextBox(),
|
||||||
bpmSlider = new BPMSlider(),
|
bpmSlider = new BPMSlider(),
|
||||||
timeSignature = new SettingsEnumDropdown<TimeSignatures>
|
timeSignature = new LabelledTimeSignature
|
||||||
{
|
{
|
||||||
LabelText = "Time Signature"
|
Label = "Time Signature"
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ using osu.Game.Screens.Menu;
|
|||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using IntroSequence = osu.Game.Configuration.IntroSequence;
|
using IntroSequence = osu.Game.Configuration.IntroSequence;
|
||||||
|
|
||||||
@ -63,6 +64,11 @@ namespace osu.Game.Screens
|
|||||||
|
|
||||||
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
|
protected virtual ShaderPrecompiler CreateShaderPrecompiler() => new ShaderPrecompiler();
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private DatabaseContextFactory efContextFactory { get; set; }
|
||||||
|
|
||||||
|
private EFToRealmMigrator realmMigrator;
|
||||||
|
|
||||||
public override void OnEntering(IScreen last)
|
public override void OnEntering(IScreen last)
|
||||||
{
|
{
|
||||||
base.OnEntering(last);
|
base.OnEntering(last);
|
||||||
@ -70,6 +76,10 @@ namespace osu.Game.Screens
|
|||||||
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
LoadComponentAsync(precompiler = CreateShaderPrecompiler(), AddInternal);
|
||||||
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
LoadComponentAsync(loadableScreen = CreateLoadableScreen());
|
||||||
|
|
||||||
|
// A non-null context factory means there's still content to migrate.
|
||||||
|
if (efContextFactory != null)
|
||||||
|
LoadComponentAsync(realmMigrator = new EFToRealmMigrator(), AddInternal);
|
||||||
|
|
||||||
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
LoadComponentAsync(spinner = new LoadingSpinner(true, true)
|
||||||
{
|
{
|
||||||
Anchor = Anchor.BottomRight,
|
Anchor = Anchor.BottomRight,
|
||||||
@ -86,7 +96,7 @@ namespace osu.Game.Screens
|
|||||||
|
|
||||||
private void checkIfLoaded()
|
private void checkIfLoaded()
|
||||||
{
|
{
|
||||||
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling)
|
if (loadableScreen.LoadState != LoadState.Ready || !precompiler.FinishedCompiling || realmMigrator?.FinishedMigrating == false)
|
||||||
{
|
{
|
||||||
Schedule(checkIfLoaded);
|
Schedule(checkIfLoaded);
|
||||||
return;
|
return;
|
||||||
|
@ -21,6 +21,7 @@ using osu.Game.Overlays;
|
|||||||
using osu.Game.Screens.Backgrounds;
|
using osu.Game.Screens.Backgrounds;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Menu
|
namespace osu.Game.Screens.Menu
|
||||||
{
|
{
|
||||||
@ -93,28 +94,27 @@ namespace osu.Game.Screens.Menu
|
|||||||
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
|
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
|
||||||
seeya = audio.Samples.Get(SeeyaSampleName);
|
seeya = audio.Samples.Get(SeeyaSampleName);
|
||||||
|
|
||||||
ILive<BeatmapSetInfo> setInfo = null;
|
|
||||||
|
|
||||||
// if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
|
// if the user has requested not to play theme music, we should attempt to find a random beatmap from their collection.
|
||||||
if (!MenuMusic.Value)
|
if (!MenuMusic.Value)
|
||||||
{
|
{
|
||||||
var sets = beatmaps.GetAllUsableBeatmapSets();
|
realmContextFactory.Run(realm =>
|
||||||
|
|
||||||
if (sets.Count > 0)
|
|
||||||
{
|
{
|
||||||
setInfo = beatmaps.QueryBeatmapSet(s => s.ID == sets[RNG.Next(0, sets.Count - 1)].ID);
|
var usableBeatmapSets = realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected).AsRealmCollection();
|
||||||
setInfo?.PerformRead(s =>
|
|
||||||
{
|
|
||||||
if (s.Beatmaps.Count == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
initialBeatmap = beatmaps.GetWorkingBeatmap(s.Beatmaps[0]);
|
int setCount = usableBeatmapSets.Count;
|
||||||
});
|
|
||||||
}
|
if (setCount > 0)
|
||||||
|
{
|
||||||
|
var found = usableBeatmapSets[RNG.Next(0, setCount - 1)].Beatmaps.FirstOrDefault();
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
initialBeatmap = beatmaps.GetWorkingBeatmap(found);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available.
|
// we generally want a song to be playing on startup, so use the intro music even if a user has specified not to if no other track is available.
|
||||||
if (setInfo == null)
|
if (initialBeatmap == null)
|
||||||
{
|
{
|
||||||
if (!loadThemedIntro())
|
if (!loadThemedIntro())
|
||||||
{
|
{
|
||||||
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
bool loadThemedIntro()
|
bool loadThemedIntro()
|
||||||
{
|
{
|
||||||
setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
|
var setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash);
|
||||||
|
|
||||||
if (setInfo == null)
|
if (setInfo == null)
|
||||||
return false;
|
return false;
|
||||||
|
@ -94,9 +94,9 @@ namespace osu.Game.Screens.Menu
|
|||||||
if (beatIndex < 0)
|
if (beatIndex < 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (effectPoint.KiaiMode ? beatIndex % 2 == 0 : beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
flash(leftBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
||||||
if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (effectPoint.KiaiMode ? beatIndex % 2 == 1 : beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
flash(rightBox, timingPoint.BeatLength, effectPoint.KiaiMode, amplitudes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,7 +282,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
{
|
{
|
||||||
this.Delay(early_activation).Schedule(() =>
|
this.Delay(early_activation).Schedule(() =>
|
||||||
{
|
{
|
||||||
if (beatIndex % (int)timingPoint.TimeSignature == 0)
|
if (beatIndex % timingPoint.TimeSignature.Numerator == 0)
|
||||||
sampleDownbeat.Play();
|
sampleDownbeat.Play();
|
||||||
else
|
else
|
||||||
sampleBeat.Play();
|
sampleBeat.Play();
|
||||||
|
@ -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 osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -23,6 +24,12 @@ namespace osu.Game.Users.Drawables
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ShowPlaceholderOnNull = true;
|
public bool ShowPlaceholderOnNull = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Perform an action in addition to showing the country ranking.
|
||||||
|
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a flag (to maintain a consistent UX).
|
||||||
|
/// </summary>
|
||||||
|
public Action Action;
|
||||||
|
|
||||||
public UpdateableFlag(Country country = null)
|
public UpdateableFlag(Country country = null)
|
||||||
{
|
{
|
||||||
Country = country;
|
Country = country;
|
||||||
@ -52,6 +59,7 @@ namespace osu.Game.Users.Drawables
|
|||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
|
Action?.Invoke();
|
||||||
rankingsOverlay?.ShowCountry(Country);
|
rankingsOverlay?.ShowCountry(Country);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,8 @@ namespace osu.Game.Users
|
|||||||
|
|
||||||
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country)
|
||||||
{
|
{
|
||||||
Size = new Vector2(39, 26)
|
Size = new Vector2(39, 26),
|
||||||
|
Action = Action,
|
||||||
};
|
};
|
||||||
|
|
||||||
protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
|
protected SpriteIcon CreateStatusIcon() => statusIcon = new SpriteIcon
|
||||||
|
Loading…
Reference in New Issue
Block a user