1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 16:07:24 +08:00

Merge branch 'master' into taiko-hitsounding-drum-sample-player

This commit is contained in:
Dean Herbert 2023-07-05 18:44:24 +09:00
commit 4bb65a54b8
23 changed files with 341 additions and 74 deletions

View File

@ -18,6 +18,7 @@ using osu.Game.Extensions;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using Realms; using Realms;
using SharpCompress.Archives; using SharpCompress.Archives;
@ -416,6 +417,53 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestImport_ThenModifyMapWithScore_ThenImport()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
string? temp = TestResources.GetTestBeatmapForImport();
var imported = await LoadOszIntoStore(importer, realm.Realm);
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
// imitate making local changes via editor
// ReSharper disable once MethodHasAsyncOverload
realm.Write(_ =>
{
BeatmapInfo beatmap = imported.Beatmaps.First();
beatmap.Hash = "new_hash";
beatmap.ResetOnlineInfo();
});
// for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap.
// the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539).
// TODO: revisit when fixing https://github.com/ppy/osu/issues/24069.
Assert.That(imported.Beatmaps.First().Scores.Any());
var importedSecondTime = await importer.Import(new ImportTask(temp));
EnsureLoaded(realm.Realm);
// check the newly "imported" beatmap is not the original.
Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null);
Assert.That(imported.ID != importedSecondTime.ID);
var importedFirstTimeBeatmap = imported.Beatmaps.First();
var importedSecondTimeBeatmap = importedSecondTime.PerformRead(s => s.Beatmaps.First());
Assert.That(importedFirstTimeBeatmap.ID != importedSecondTimeBeatmap.ID);
Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash);
Assert.That(!importedFirstTimeBeatmap.Scores.Any());
Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1);
});
}
[Test] [Test]
public void TestImportThenImportWithChangedFile() public void TestImportThenImportWithChangedFile()
{ {
@ -1074,18 +1122,16 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending); Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
} }
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) =>
{ realm.WriteAsync(() =>
// TODO: reimplement when we have score support in realm. {
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo realm.Add(new ScoreInfo
// { {
// OnlineID = 2, OnlineID = 2,
// Beatmap = beatmap, BeatmapInfo = beatmap,
// BeatmapInfoID = beatmap.ID BeatmapHash = beatmap.Hash
// }, new ImportScoreTest.TestArchiveReader()); });
});
return Task.CompletedTask;
}
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false) private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
{ {

View File

@ -347,6 +347,73 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestDanglingScoreTransferred()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOnlineCopy);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
string scoreTargetBeatmapHash = string.Empty;
// set a score on the beatmap
importBeforeUpdate.PerformWrite(s =>
{
var beatmapInfo = s.Beatmaps.First();
scoreTargetBeatmapHash = beatmapInfo.Hash;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
// locally modify beatmap
const string new_beatmap_hash = "new_hash";
importBeforeUpdate.PerformWrite(s =>
{
var beatmapInfo = s.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash);
beatmapInfo.Hash = new_beatmap_hash;
beatmapInfo.ResetOnlineInfo();
});
realm.Run(r => r.Refresh());
// for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap.
// the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (https://github.com/ppy/osu/pull/22539).
// TODO: revisit when fixing https://github.com/ppy/osu/issues/24069.
checkCount<ScoreInfo>(realm, 1);
// reimport the original beatmap before local modifications
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value);
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
// both original and locally modified versions present
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
checkCount<BeatmapSetInfo>(realm, 2);
// score is preserved
checkCount<ScoreInfo>(realm, 1);
// score is transferred to new beatmap
Assert.That(importBeforeUpdate.Value.Beatmaps.First(b => b.Hash == new_beatmap_hash).Scores, Has.Count.EqualTo(0));
Assert.That(importAfterUpdate.Value.Beatmaps.First(b => b.Hash == scoreTargetBeatmapHash).Scores, Has.Count.EqualTo(1));
});
}
[Test] [Test]
public void TestScoreLostOnModification() public void TestScoreLostOnModification()
{ {

View File

@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay
ManualClock clock = null; ManualClock clock = null;
var beatmap = new Beatmap(); var beatmap = new Beatmap();
beatmap.HitObjects.Add(new TestHitObjectWithNested { Duration = 40 }); beatmap.HitObjects.Add(new TestHitObjectWithNested
{
Duration = 40,
NestedObjects = new HitObject[]
{
new PooledNestedHitObject { StartTime = 10 },
new PooledNestedHitObject { StartTime = 20 },
new PooledNestedHitObject { StartTime = 30 }
}
});
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
@ -209,6 +218,49 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("object judged", () => playfield.JudgedObjects.Count == 1); AddAssert("object judged", () => playfield.JudgedObjects.Count == 1);
} }
[Test]
public void TestPooledObjectWithNonPooledNesteds()
{
ManualClock clock = null;
TestHitObjectWithNested hitObjectWithNested;
var beatmap = new Beatmap();
beatmap.HitObjects.Add(hitObjectWithNested = new TestHitObjectWithNested
{
Duration = 40,
NestedObjects = new HitObject[]
{
new PooledNestedHitObject { StartTime = 10 },
new NonPooledNestedHitObject { StartTime = 20 },
new NonPooledNestedHitObject { StartTime = 30 }
}
});
createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock()));
AddAssert("hitobject entry has all nesteds", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(3));
AddStep("skip to middle of object", () => clock.CurrentTime = (hitObjectWithNested.StartTime + hitObjectWithNested.GetEndTime()) / 2);
AddAssert("2 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(2));
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
AddStep("skip to before end of object", () => clock.CurrentTime = hitObjectWithNested.GetEndTime() - 1);
AddAssert("3 objects judged", () => playfield.JudgedObjects.Count, () => Is.EqualTo(3));
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
AddStep("removing object doesn't crash", () => playfield.Remove(hitObjectWithNested));
AddStep("clear judged", () => playfield.JudgedObjects.Clear());
AddStep("add object back", () => playfield.Add(hitObjectWithNested));
AddAssert("entry not all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.False);
AddStep("skip to long past object", () => clock.CurrentTime = 100_000);
// the parent entry should still be linked to nested entries of pooled objects that are managed externally
// but not contain synthetic entries that were created for the non-pooled objects.
AddAssert("entry still has non-synthetic nested entries", () => playfield.HitObjectContainer.Entries.Single().NestedEntries, () => Has.Count.EqualTo(1));
AddAssert("entry all judged", () => playfield.HitObjectContainer.Entries.Single().AllJudged, () => Is.True);
}
private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null) private void createTest(IBeatmap beatmap, int poolSize, Func<IFrameBasedClock> createClock = null)
{ {
AddStep("create test", () => AddStep("create test", () =>
@ -289,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize); RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize); RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize); RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
RegisterPool<NestedHitObject, DrawableNestedHitObject>(poolSize); RegisterPool<PooledNestedHitObject, DrawableNestedHitObject>(poolSize);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@ -422,16 +474,22 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestHitObjectWithNested : TestHitObject private class TestHitObjectWithNested : TestHitObject
{ {
public IEnumerable<HitObject> NestedObjects { get; init; } = Array.Empty<HitObject>();
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(cancellationToken); base.CreateNestedHitObjects(cancellationToken);
for (int i = 0; i < 3; ++i) foreach (var ho in NestedObjects)
AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 }); AddNested(ho);
} }
} }
private class NestedHitObject : ConvertHitObject private class PooledNestedHitObject : ConvertHitObject
{
}
private class NonPooledNestedHitObject : ConvertHitObject
{ {
} }
@ -482,6 +540,9 @@ namespace osu.Game.Tests.Visual.Gameplay
nestedContainer.Clear(false); nestedContainer.Clear(false);
} }
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
=> hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null;
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
base.CheckForResult(userTriggered, timeOffset); base.CheckForResult(userTriggered, timeOffset);
@ -490,25 +551,30 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
private partial class DrawableNestedHitObject : DrawableHitObject<NestedHitObject> private partial class DrawableNestedHitObject : DrawableHitObject
{ {
public DrawableNestedHitObject() public DrawableNestedHitObject()
: this(null)
{ {
} }
public DrawableNestedHitObject(NestedHitObject hitObject) public DrawableNestedHitObject(PooledNestedHitObject hitObject)
: base(hitObject)
{
}
public DrawableNestedHitObject(NonPooledNestedHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
Size = new Vector2(15);
Colour = Colour4.White;
RelativePositionAxes = Axes.Both;
Origin = Anchor.Centre;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Size = new Vector2(15);
Colour = Colour4.White;
RelativePositionAxes = Axes.Both;
Origin = Anchor.Centre;
AddInternal(new Circle AddInternal(new Circle
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -107,6 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestBeatmapDownloadingStates() public void TestBeatmapDownloadingStates()
{ {
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
@ -382,6 +383,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}); });
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true); AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
AddStep("set beatmap available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
} }
private void checkProgressBarVisibility(bool visible) => private void checkProgressBarVisibility(bool visible) =>

View File

@ -20,6 +20,7 @@ using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms; using Realms;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
@ -204,6 +205,15 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters) protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters parameters)
{ {
base.PostImport(model, realm, parameters); base.PostImport(model, realm, parameters);
// Scores are stored separately from beatmaps, and persist even when a beatmap is modified or deleted.
// Let's reattach any matching scores that exist in the database, based on hash.
foreach (BeatmapInfo beatmap in model.Beatmaps)
{
foreach (var score in realm.All<ScoreInfo>().Where(score => score.BeatmapHash == beatmap.Hash))
score.BeatmapInfo = beatmap;
}
ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst); ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);
} }

View File

@ -188,7 +188,7 @@ namespace osu.Game.Collections
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, X = -OsuScrollContainer.SCROLL_BAR_WIDTH,
Scale = new Vector2(0.65f), Scale = new Vector2(0.65f),
Action = addOrRemove, Action = addOrRemove,
}); });

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers
public partial class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable public partial class OsuScrollContainer<T> : ScrollContainer<T> where T : Drawable
{ {
public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_WIDTH = 10;
public const float SCROLL_BAR_PADDING = 3; public const float SCROLL_BAR_PADDING = 3;
/// <summary> /// <summary>
@ -139,6 +139,8 @@ namespace osu.Game.Graphics.Containers
private readonly Box box; private readonly Box box;
protected override float MinimumDimSize => SCROLL_BAR_WIDTH * 3;
public OsuScrollbar(Direction scrollDir) public OsuScrollbar(Direction scrollDir)
: base(scrollDir) : base(scrollDir)
{ {
@ -147,7 +149,7 @@ namespace osu.Game.Graphics.Containers
CornerRadius = 5; CornerRadius = 5;
// needs to be set initially for the ResizeTo to respect minimum size // needs to be set initially for the ResizeTo to respect minimum size
Size = new Vector2(SCROLL_BAR_HEIGHT); Size = new Vector2(SCROLL_BAR_WIDTH);
const float margin = 3; const float margin = 3;
@ -173,11 +175,10 @@ namespace osu.Game.Graphics.Containers
public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None)
{ {
Vector2 size = new Vector2(SCROLL_BAR_HEIGHT) this.ResizeTo(new Vector2(SCROLL_BAR_WIDTH)
{ {
[(int)ScrollDirection] = val [(int)ScrollDirection] = val
}; }, duration, easing);
this.ResizeTo(size, duration, easing);
} }
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in"); public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
/// <summary>
/// "Sign out"
/// </summary>
public static LocalisableString SignOut => new TranslatableString(getKey(@"sign_out"), @"Sign out");
/// <summary> /// <summary>
/// "Account" /// "Account"
/// </summary> /// </summary>

View File

@ -29,7 +29,7 @@ namespace osu.Game.Online.Multiplayer
/// The availability state of the current beatmap. /// The availability state of the current beatmap.
/// </summary> /// </summary>
[Key(2)] [Key(2)]
public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.Unknown();
/// <summary> /// <summary>
/// Any mods applicable only to the local user. /// Any mods applicable only to the local user.

View File

@ -34,6 +34,7 @@ namespace osu.Game.Online.Rooms
DownloadProgress = downloadProgress; DownloadProgress = downloadProgress;
} }
public static BeatmapAvailability Unknown() => new BeatmapAvailability(DownloadState.Unknown);
public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded);
public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress); public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress);
public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing); public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing);

View File

@ -60,6 +60,15 @@ namespace osu.Game.Online.Rooms
if (item.NewValue == null) if (item.NewValue == null)
return; return;
// Initially set to unknown until we have attained a good state.
// This has the wanted side effect of forcing a state change when the current playlist
// item changes at the server but our local availability doesn't necessarily change
// (ie. we have both the previous and next item LocallyAvailable).
//
// Note that even without this, the server will trigger a state change and things will work.
// This is just for safety.
availability.Value = BeatmapAvailability.Unknown();
downloadTracker?.RemoveAndDisposeImmediately(); downloadTracker?.RemoveAndDisposeImmediately();
selectedBeatmap = null; selectedBeatmap = null;
@ -115,6 +124,9 @@ namespace osu.Game.Online.Rooms
switch (downloadTracker.State.Value) switch (downloadTracker.State.Value)
{ {
case DownloadState.Unknown: case DownloadState.Unknown:
availability.Value = BeatmapAvailability.Unknown();
break;
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
availability.Value = BeatmapAvailability.NotDownloaded(); availability.Value = BeatmapAvailability.NotDownloaded();
break; break;

View File

@ -1164,7 +1164,9 @@ namespace osu.Game
private void forwardTabletLogsToNotifications() private void forwardTabletLogsToNotifications()
{ {
const string tablet_prefix = @"[Tablet] "; const string tablet_prefix = @"[Tablet] ";
bool notifyOnWarning = true; bool notifyOnWarning = true;
bool notifyOnError = true;
Logger.NewEntry += entry => Logger.NewEntry += entry =>
{ {
@ -1175,6 +1177,11 @@ namespace osu.Game
if (entry.Level == LogLevel.Error) if (entry.Level == LogLevel.Error)
{ {
if (!notifyOnError)
return;
notifyOnError = false;
Schedule(() => Schedule(() =>
{ {
Notifications.Post(new SimpleNotification Notifications.Post(new SimpleNotification
@ -1213,7 +1220,11 @@ namespace osu.Game
Schedule(() => Schedule(() =>
{ {
ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault(); ITabletHandler tablet = Host.AvailableInputHandlers.OfType<ITabletHandler>().SingleOrDefault();
tablet?.Tablet.BindValueChanged(_ => notifyOnWarning = true, true); tablet?.Tablet.BindValueChanged(_ =>
{
notifyOnWarning = true;
notifyOnError = true;
}, true);
}); });
} }

View File

@ -44,6 +44,6 @@ namespace osu.Game.Overlays
/// <summary> /// <summary>
/// All ongoing operations (ie. any <see cref="ProgressNotification"/> not in a completed state). /// All ongoing operations (ie. any <see cref="ProgressNotification"/> not in a completed state).
/// </summary> /// </summary>
public IEnumerable<ProgressNotification> OngoingOperations => AllNotifications.OfType<ProgressNotification>().Where(p => p.State != ProgressNotificationState.Completed); public IEnumerable<ProgressNotification> OngoingOperations => AllNotifications.OfType<ProgressNotification>().Where(p => p.State != ProgressNotificationState.Completed && p.State != ProgressNotificationState.Cancelled);
} }
} }

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Login
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))] [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
AppearOffline, AppearOffline,
[LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))] [LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))]
SignOut, SignOut,
} }
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Overlays
scrollbarBackground = new Box scrollbarBackground = new Box
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = OsuScrollContainer.SCROLL_BAR_HEIGHT, Width = OsuScrollContainer.SCROLL_BAR_WIDTH,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Alpha = 0.5f Alpha = 0.5f

View File

@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual bool DisplayResult => true; public virtual bool DisplayResult => true;
/// <summary> /// <summary>
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged. /// The scoring result of this <see cref="DrawableHitObject"/>.
/// </summary> /// </summary>
public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged); public JudgementResult Result => Entry?.Result;
/// <summary> /// <summary>
/// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit. /// Whether this <see cref="DrawableHitObject"/> has been hit. This occurs if <see cref="Result"/> is hit.
@ -112,12 +112,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// Whether this <see cref="DrawableHitObject"/> has been judged. /// Whether this <see cref="DrawableHitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects. /// Note: This does NOT include nested hitobjects.
/// </summary> /// </summary>
public bool Judged => Result?.HasResult ?? true; public bool Judged => Entry?.Judged ?? true;
/// <summary> /// <summary>
/// The scoring result of this <see cref="DrawableHitObject"/>. /// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
/// </summary> /// </summary>
public JudgementResult Result => Entry?.Result; public bool AllJudged => Entry?.AllJudged ?? true;
/// <summary> /// <summary>
/// The relative X position of this hit object for sample playback balance adjustment. /// The relative X position of this hit object for sample playback balance adjustment.
@ -218,6 +218,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected sealed override void OnApply(HitObjectLifetimeEntry entry) protected sealed override void OnApply(HitObjectLifetimeEntry entry)
{ {
Debug.Assert(Entry != null);
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset. // LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO. // We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
if (entry is SyntheticHitObjectEntry) if (entry is SyntheticHitObjectEntry)
@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
drawableNested.ParentHitObject = this; drawableNested.ParentHitObject = this;
nestedHitObjects.Add(drawableNested); nestedHitObjects.Add(drawableNested);
// assume that synthetic entries are not pooled and therefore need to be managed from within the DHO.
// this is important for the correctness of value of flags such as `AllJudged`.
if (drawableNested.Entry is SyntheticHitObjectEntry syntheticNestedEntry)
Entry.NestedEntries.Add(syntheticNestedEntry);
AddNestedHitObject(drawableNested); AddNestedHitObject(drawableNested);
} }
@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected sealed override void OnFree(HitObjectLifetimeEntry entry) protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{ {
Debug.Assert(Entry != null);
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo) if (HitObject is IHasComboInformation combo)
@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
nestedHitObjects.Clear(); nestedHitObjects.Clear();
// clean up synthetic entries manually added in `Apply()`.
Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry);
ClearNestedHitObjects(); ClearNestedHitObjects();
HitObject.DefaultsApplied -= onDefaultsApplied; HitObject.DefaultsApplied -= onDefaultsApplied;

View File

@ -2,6 +2,8 @@
// 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;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -19,12 +21,28 @@ namespace osu.Game.Rulesets.Objects
/// </summary> /// </summary>
public readonly HitObject HitObject; public readonly HitObject HitObject;
/// <summary>
/// The list of <see cref="HitObjectLifetimeEntry"/> for the <see cref="HitObject"/>'s nested objects (if any).
/// </summary>
public List<HitObjectLifetimeEntry> NestedEntries { get; internal set; } = new List<HitObjectLifetimeEntry>();
/// <summary> /// <summary>
/// The result that <see cref="HitObject"/> was judged with. /// The result that <see cref="HitObject"/> was judged with.
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding. /// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
/// </summary> /// </summary>
internal JudgementResult? Result; internal JudgementResult? Result;
/// <summary>
/// Whether <see cref="HitObject"/> has been judged.
/// Note: This does NOT include nested hitobjects.
/// </summary>
public bool Judged => Result?.HasResult ?? true;
/// <summary>
/// Whether <see cref="HitObject"/> and all of its nested objects have been judged.
/// </summary>
public bool AllJudged => Judged && NestedEntries.All(h => h.AllJudged);
private readonly IBindable<double> startTimeBindable = new BindableDouble(); private readonly IBindable<double> startTimeBindable = new BindableDouble();
internal event Action? RevertResult; internal event Action? RevertResult;

View File

@ -43,11 +43,6 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// </remarks> /// </remarks>
private readonly Dictionary<HitObjectLifetimeEntry, HitObject> parentMap = new Dictionary<HitObjectLifetimeEntry, HitObject>(); private readonly Dictionary<HitObjectLifetimeEntry, HitObject> parentMap = new Dictionary<HitObjectLifetimeEntry, HitObject>();
/// <summary>
/// Stores the list of child entries for each hit object managed by this <see cref="HitObjectEntryManager"/>.
/// </summary>
private readonly Dictionary<HitObject, List<HitObjectLifetimeEntry>> childrenMap = new Dictionary<HitObject, List<HitObjectLifetimeEntry>>();
public void Add(HitObjectLifetimeEntry entry, HitObject? parent) public void Add(HitObjectLifetimeEntry entry, HitObject? parent)
{ {
HitObject hitObject = entry.HitObject; HitObject hitObject = entry.HitObject;
@ -57,22 +52,24 @@ namespace osu.Game.Rulesets.Objects.Pooling
// Add the entry. // Add the entry.
entryMap[hitObject] = entry; entryMap[hitObject] = entry;
childrenMap[hitObject] = new List<HitObjectLifetimeEntry>();
// If the entry has a parent, set it and add the entry to the parent's children. // If the entry has a parent, set it and add the entry to the parent's children.
if (parent != null) if (parent != null)
{ {
parentMap[entry] = parent; parentMap[entry] = parent;
if (childrenMap.TryGetValue(parent, out var parentChildEntries)) if (entryMap.TryGetValue(parent, out var parentEntry))
parentChildEntries.Add(entry); parentEntry.NestedEntries.Add(entry);
} }
hitObject.DefaultsApplied += onDefaultsApplied; hitObject.DefaultsApplied += onDefaultsApplied;
OnEntryAdded?.Invoke(entry, parent); OnEntryAdded?.Invoke(entry, parent);
} }
public void Remove(HitObjectLifetimeEntry entry) public bool Remove(HitObjectLifetimeEntry entry)
{ {
if (entry is SyntheticHitObjectEntry)
return false;
HitObject hitObject = entry.HitObject; HitObject hitObject = entry.HitObject;
if (!entryMap.ContainsKey(hitObject)) if (!entryMap.ContainsKey(hitObject))
@ -81,18 +78,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
entryMap.Remove(hitObject); entryMap.Remove(hitObject);
// If the entry has a parent, unset it and remove the entry from the parents' children. // If the entry has a parent, unset it and remove the entry from the parents' children.
if (parentMap.Remove(entry, out var parent) && childrenMap.TryGetValue(parent, out var parentChildEntries)) if (parentMap.Remove(entry, out var parent) && entryMap.TryGetValue(parent, out var parentEntry))
parentChildEntries.Remove(entry); parentEntry.NestedEntries.Remove(entry);
// Remove all the entries' children. // Remove all the entries' children.
if (childrenMap.Remove(hitObject, out var childEntries)) foreach (var childEntry in entry.NestedEntries)
{ Remove(childEntry);
foreach (var childEntry in childEntries)
Remove(childEntry);
}
hitObject.DefaultsApplied -= onDefaultsApplied; hitObject.DefaultsApplied -= onDefaultsApplied;
OnEntryRemoved?.Invoke(entry, parent); OnEntryRemoved?.Invoke(entry, parent);
return true;
} }
public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry) public bool TryGet(HitObject hitObject, [MaybeNullWhen(false)] out HitObjectLifetimeEntry entry)
@ -105,16 +100,16 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// </summary> /// </summary>
private void onDefaultsApplied(HitObject hitObject) private void onDefaultsApplied(HitObject hitObject)
{ {
if (!childrenMap.Remove(hitObject, out var childEntries)) if (!entryMap.TryGetValue(hitObject, out var entry))
return; return;
// Remove all the entries' children. At this point the parents' (this entries') children list has been removed from the map, so this does not cause upwards traversal. // Replace the entire list rather than clearing to prevent circular traversal later.
foreach (var entry in childEntries) var previousEntries = entry.NestedEntries;
Remove(entry); entry.NestedEntries = new List<HitObjectLifetimeEntry>();
// The removed children list needs to be added back to the map for the entry to potentially receive children. // Remove all the entries' children. At this point the parents' (this entries') children list has been reconstructed, so this does not cause upwards traversal.
childEntries.Clear(); foreach (var nested in previousEntries)
childrenMap[hitObject] = childEntries; Remove(nested);
} }
} }
} }

View File

@ -82,6 +82,7 @@ namespace osu.Game.Scoring
{ {
Ruleset = ruleset ?? new RulesetInfo(); Ruleset = ruleset ?? new RulesetInfo();
BeatmapInfo = beatmap ?? new BeatmapInfo(); BeatmapInfo = beatmap ?? new BeatmapInfo();
BeatmapHash = BeatmapInfo.Hash;
RealmUser = realmUser ?? new RealmUser(); RealmUser = realmUser ?? new RealmUser();
ID = Guid.NewGuid(); ID = Guid.NewGuid();
} }

View File

@ -377,10 +377,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X; float leftExcess = thisQuad.TopLeft.X - parentQuad.TopLeft.X;
float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X; float rightExcess = parentQuad.TopRight.X - thisQuad.TopRight.X;
if (topExcess + bottomExcess < buttons.Height + button_padding) float minHeight = buttons.ScreenSpaceDrawQuad.Height;
if (topExcess < minHeight && bottomExcess < minHeight)
{ {
buttons.Anchor = Anchor.BottomCentre; buttons.Anchor = Anchor.BottomCentre;
buttons.Origin = Anchor.BottomCentre; buttons.Origin = Anchor.BottomCentre;
buttons.Y = Math.Min(0, ToLocalSpace(Parent.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
} }
else if (topExcess > bottomExcess) else if (topExcess > bottomExcess)
{ {

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
scroll.RelativeSizeAxes = Axes.X; scroll.RelativeSizeAxes = Axes.X;
scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_HEIGHT + OsuScrollContainer.SCROLL_BAR_PADDING * 2; scroll.Height = ParticipantsList.TILE_SIZE + OsuScrollContainer.SCROLL_BAR_WIDTH + OsuScrollContainer.SCROLL_BAR_PADDING * 2;
list.RelativeSizeAxes = Axes.Y; list.RelativeSizeAxes = Axes.Y;
list.AutoSizeAxes = Axes.X; list.AutoSizeAxes = Axes.X;

View File

@ -313,16 +313,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget(); client.ChangeBeatmapAvailability(availability.NewValue).FireAndForget();
if (availability.NewValue.State != DownloadState.LocallyAvailable) switch (availability.NewValue.State)
{ {
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. case DownloadState.LocallyAvailable:
if (client.LocalUser?.State == MultiplayerUserState.Ready) if (client.LocalUser?.State == MultiplayerUserState.Spectating
client.ChangeState(MultiplayerUserState.Idle); && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
} {
else if (client.LocalUser?.State == MultiplayerUserState.Spectating onLoadRequested();
&& (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing)) }
{
onLoadRequested(); break;
case DownloadState.Unknown:
// Don't do anything rash in an unknown state.
break;
default:
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
break;
} }
} }

View File

@ -154,6 +154,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
this.FadeOut(fade_time); this.FadeOut(fade_time);
break; break;
case DownloadState.Unknown:
text.Text = "checking availability";
icon.Icon = FontAwesome.Solid.Question;
icon.Colour = colours.Orange0;
break;
case DownloadState.NotDownloaded: case DownloadState.NotDownloaded:
text.Text = "no map"; text.Text = "no map";
icon.Icon = FontAwesome.Solid.MinusCircle; icon.Icon = FontAwesome.Solid.MinusCircle;