1
0
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:
Dean Herbert 2022-01-24 18:25:47 +09:00
commit 9ff9611296
30 changed files with 461 additions and 116 deletions

View File

@ -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;

View File

@ -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);

View File

@ -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);
} }
} }

View File

@ -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");
} }
} }

View File

@ -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");
}
}
}

View File

@ -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));
} }
} }
} }

View File

@ -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);
}
} }
} }

View File

@ -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);
} }

View File

@ -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>

View File

@ -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;

View File

@ -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)

View File

@ -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},"));

View 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;
}
}

View File

@ -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,

View File

@ -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);

View File

@ -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);
}
} }
} }

View File

@ -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);

View File

@ -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)

View File

@ -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:

View File

@ -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
}); });
} }
} }

View File

@ -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);

View 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();
}
}
}
}
}

View File

@ -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)

View File

@ -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"
}, }
}); });
} }

View File

@ -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;

View File

@ -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;

View File

@ -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);
} }

View File

@ -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();

View File

@ -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;
} }

View File

@ -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