diff --git a/osu.Android.props b/osu.Android.props
index 2c186a52dd..77c29a5d6e 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,7 +52,7 @@
-
+
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
index 0306c99fd5..1a9d12e860 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs
@@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-#nullable disable
-
using System;
+using System.Diagnostics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Objects;
@@ -21,32 +20,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
public const int SPACING = 32;
public const double PREEMPT = 800;
- public DrawablePool Pool;
+ public DrawablePool? Pool { private get; set; }
protected override void OnApply(FollowPointLifetimeEntry entry)
{
base.OnApply(entry);
- entry.Invalidated += onEntryInvalidated;
- refreshPoints();
+ entry.Invalidated += scheduleRefresh;
+
+ // Our clock may not be correct at this point if `LoadComplete` has not run yet.
+ // Without a schedule, animations referencing FollowPoint's clock (see `IAnimationTimeReference`) would be incorrect on first pool usage.
+ scheduleRefresh();
}
protected override void OnFree(FollowPointLifetimeEntry entry)
{
base.OnFree(entry);
- entry.Invalidated -= onEntryInvalidated;
+ entry.Invalidated -= scheduleRefresh;
// Return points to the pool.
ClearInternal(false);
}
- private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
-
- private void refreshPoints()
+ private void scheduleRefresh() => Scheduler.AddOnce(() =>
{
+ Debug.Assert(Pool != null);
+
ClearInternal(false);
var entry = Entry;
+
if (entry?.End == null) return;
OsuHitObject start = entry.Start;
@@ -95,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
}
entry.LifetimeEnd = finalTransformEndTime;
- }
+ });
///
/// Computes the fade time of follow point positioned between two hitobjects.
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
index ce418f33f0..1858aee76b 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs
@@ -3,6 +3,7 @@
#nullable disable
+using System;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -66,6 +67,18 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Scroll container is loaded", () => scrollContainer.LoadState >= LoadState.Loaded);
}
+ [Test]
+ public void TestInitialZoomOutOfRange()
+ {
+ AddStep("Invalid ZoomableScrollContainer throws ArgumentException", () =>
+ {
+ Assert.Throws(() =>
+ {
+ _ = new ZoomableScrollContainer(1, 60, 0);
+ });
+ });
+ }
+
[Test]
public void TestWidthInitialization()
{
diff --git a/osu.Game/Database/Live.cs b/osu.Game/Database/Live.cs
index 52e1d420f7..3bb11c3a50 100644
--- a/osu.Game/Database/Live.cs
+++ b/osu.Game/Database/Live.cs
@@ -51,7 +51,15 @@ namespace osu.Game.Database
ID = id;
}
- public bool Equals(Live? other) => ID == other?.ID;
+ public bool Equals(Live? other)
+ {
+ if (ReferenceEquals(this, other)) return true;
+ if (other == null) return false;
+
+ return ID == other.ID;
+ }
+
+ public override int GetHashCode() => HashCode.Combine(ID);
public override string ToString() => PerformRead(i => i.ToString());
}
diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
index 30433ab8cd..c870157fec 100644
--- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
+++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs
@@ -10,6 +10,9 @@ using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
+ ///
+ /// Represents an aggregate score for a user based off all beatmaps that have been played in the playlist.
+ ///
public class APIUserScoreAggregate
{
[JsonProperty("attempts")]
diff --git a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
index 2f3ece0e3b..f51f57c031 100644
--- a/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
+++ b/osu.Game/Online/Leaderboards/LeaderboardScoreTooltip.cs
@@ -15,6 +15,8 @@ using osu.Framework.Localisation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
+using osu.Framework.Bindables;
+using osu.Game.Configuration;
namespace osu.Game.Online.Leaderboards
{
@@ -24,6 +26,7 @@ namespace osu.Game.Online.Leaderboards
private FillFlowContainer topScoreStatistics = null!;
private FillFlowContainer bottomScoreStatistics = null!;
private FillFlowContainer modStatistics = null!;
+ private readonly Bindable prefer24HourTime = new Bindable();
public LeaderboardScoreTooltip()
{
@@ -36,8 +39,9 @@ namespace osu.Game.Online.Leaderboards
}
[BackgroundDependencyLoader]
- private void load(OsuColour colours)
+ private void load(OsuColour colours, OsuConfigManager configManager)
{
+ configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
InternalChildren = new Drawable[]
{
new Box
@@ -92,6 +96,13 @@ namespace osu.Game.Online.Leaderboards
};
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ prefer24HourTime.BindValueChanged(_ => updateTimestampLabel(), true);
+ }
+
private ScoreInfo? displayedScore;
public void SetContent(ScoreInfo score)
@@ -101,7 +112,7 @@ namespace osu.Game.Online.Leaderboards
displayedScore = score;
- timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
+ updateTimestampLabel();
modStatistics.Clear();
topScoreStatistics.Clear();
@@ -121,6 +132,16 @@ namespace osu.Game.Online.Leaderboards
}
}
+ private void updateTimestampLabel()
+ {
+ if (displayedScore != null)
+ {
+ timestampLabel.Text = prefer24HourTime.Value
+ ? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
+ : $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
+ }
+ }
+
protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 9e2384322a..0d115f62fe 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -56,14 +56,12 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
-using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Graphics;
using Sentry;
-using Logger = osu.Framework.Logging.Logger;
namespace osu.Game
{
@@ -294,25 +292,13 @@ namespace osu.Game
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName;
- // bind config int to database SkinInfo
configSkin = LocalConfig.GetBindable(OsuSetting.Skin);
+
+ // Transfer skin from config to realm instance once on startup.
+ SkinManager.SetSkinFromConfiguration(configSkin.Value);
+
+ // Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
- configSkin.ValueChanged += skinId =>
- {
- Live skinInfo = null;
-
- if (Guid.TryParse(skinId.NewValue, out var guid))
- skinInfo = SkinManager.Query(s => s.ID == guid);
-
- if (skinInfo == null)
- {
- if (guid == SkinInfo.CLASSIC_SKIN)
- skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged();
- }
-
- SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged();
- };
- configSkin.TriggerChange();
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index d23ef7e3e7..4787b07af8 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Framework.Platform;
-using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
@@ -36,9 +35,6 @@ namespace osu.Game.Overlays.Settings.Sections
Icon = FontAwesome.Solid.PaintBrush
};
- private readonly Bindable> dropdownBindable = new Bindable> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() };
- private readonly Bindable configBindable = new Bindable();
-
private static readonly Live random_skin_info = new SkinInfo
{
ID = SkinInfo.RANDOM_SKIN,
@@ -56,13 +52,14 @@ namespace osu.Game.Overlays.Settings.Sections
private IDisposable realmSubscription;
[BackgroundDependencyLoader(permitNulls: true)]
- private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
+ private void load([CanBeNull] SkinEditorOverlay skinEditor)
{
Children = new Drawable[]
{
skinDropdown = new SkinSettingsDropdown
{
LabelText = SkinSettingsStrings.CurrentSkin,
+ Current = skins.CurrentSkinInfo,
Keywords = new[] { @"skins" }
},
new SettingsButton
@@ -73,47 +70,28 @@ namespace osu.Game.Overlays.Settings.Sections
new ExportSkinButton(),
new DeleteSkinButton(),
};
-
- config.BindWith(OsuSetting.Skin, configBindable);
}
protected override void LoadComplete()
{
base.LoadComplete();
- skinDropdown.Current = dropdownBindable;
-
realmSubscription = realm.RegisterForNotifications(_ => realm.Realm.All()
.Where(s => !s.DeletePending)
.OrderByDescending(s => s.Protected) // protected skins should be at the top.
.ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged);
- configBindable.BindValueChanged(_ => Scheduler.AddOnce(updateSelectedSkinFromConfig));
-
- dropdownBindable.BindValueChanged(dropdownSelectionChanged);
- }
-
- private void dropdownSelectionChanged(ValueChangedEvent> skin)
- {
- // Only handle cases where it's clear the user has intent to change skins.
- if (skin.OldValue == null) return;
-
- if (skin.NewValue.Equals(random_skin_info))
+ skinDropdown.Current.BindValueChanged(skin =>
{
- var skinBefore = skins.CurrentSkinInfo.Value;
-
- skins.SelectRandomSkin();
-
- if (skinBefore == skins.CurrentSkinInfo.Value)
+ if (skin.NewValue == random_skin_info)
{
- // the random selection didn't change the skin, so we should manually update the dropdown to match.
- dropdownBindable.Value = skins.CurrentSkinInfo.Value;
+ // before selecting random, set the skin back to the previous selection.
+ // this is done because at this point it will be random_skin_info, and would
+ // cause SelectRandomSkin to be unable to skip the previous selection.
+ skins.CurrentSkinInfo.Value = skin.OldValue;
+ skins.SelectRandomSkin();
}
-
- return;
- }
-
- configBindable.Value = skin.NewValue.ID.ToString();
+ });
}
private void skinsChanged(IRealmCollection sender, ChangeSet changes, Exception error)
@@ -132,25 +110,7 @@ namespace osu.Game.Overlays.Settings.Sections
dropdownItems.Add(skin.ToLive(realm));
dropdownItems.Insert(protectedCount, random_skin_info);
- Schedule(() =>
- {
- skinDropdown.Items = dropdownItems;
-
- updateSelectedSkinFromConfig();
- });
- }
-
- private void updateSelectedSkinFromConfig()
- {
- if (!skinDropdown.Items.Any())
- return;
-
- Live skin = null;
-
- if (Guid.TryParse(configBindable.Value, out var configId))
- skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId);
-
- dropdownBindable.Value = skin ?? skinDropdown.Items.First();
+ Schedule(() => skinDropdown.Items = dropdownItems);
}
protected override void Dispose(bool isDisposing)
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
index 9e96a7386d..721f0c4e3b 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs
@@ -196,7 +196,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
- float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
+ float initialZoom = (float)(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom));
float minimumZoom = getZoomLevelForVisibleMilliseconds(10000);
float maximumZoom = getZoomLevelForVisibleMilliseconds(500);
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
index 7d51284f46..0fb59a8a1f 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs
@@ -87,6 +87,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (minimum > maximum)
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
+ if (initial < minimum || initial > maximum)
+ throw new ArgumentException($"{nameof(initial)} ({initial}) must be between {nameof(minimum)} ({minimum}) and {nameof(maximum)} ({maximum})");
+
minZoom = minimum;
maxZoom = maximum;
CurrentZoom = zoomTarget = initial;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
index 1f80c47d13..5a297f18db 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs
@@ -110,6 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (Client != null)
{
Client.RoomUpdated -= invokeOnRoomUpdated;
+ Client.LoadRequested -= invokeOnRoomLoadRequested;
Client.UserLeft -= invokeUserLeft;
Client.UserKicked -= invokeUserKicked;
Client.UserJoined -= invokeUserJoined;
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index f677cebe51..7ffea3b54f 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -119,7 +119,9 @@ namespace osu.Game.Skinning
Realm.Run(r =>
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
- var randomChoices = r.All().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
+ var randomChoices = r.All()
+ .Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID)
+ .ToArray();
if (randomChoices.Length == 0)
{
@@ -297,5 +299,21 @@ namespace osu.Game.Skinning
Delete(items.ToList(), silent);
});
}
+
+ public void SetSkinFromConfiguration(string guidString)
+ {
+ Live skinInfo = null;
+
+ if (Guid.TryParse(guidString, out var guid))
+ skinInfo = Query(s => s.ID == guid);
+
+ if (skinInfo == null)
+ {
+ if (guid == SkinInfo.CLASSIC_SKIN)
+ skinInfo = DefaultLegacySkin.SkinInfo;
+ }
+
+ CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.SkinInfo;
+ }
}
}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index fed7c27f07..29e690a024 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -35,7 +35,7 @@
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index 496bfbb85c..83410b08f6 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -61,7 +61,7 @@
-
+
@@ -82,7 +82,7 @@
-
+