1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 13:27:25 +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.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Resources;
using Realms;
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]
public void TestImportThenImportWithChangedFile()
{
@ -1074,18 +1122,16 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(realm.All<BeatmapSetInfo>().First(_ => true).DeletePending);
}
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap)
{
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
// OnlineID = 2,
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());
return Task.CompletedTask;
}
private static Task createScoreForBeatmap(Realm realm, BeatmapInfo beatmap) =>
realm.WriteAsync(() =>
{
realm.Add(new ScoreInfo
{
OnlineID = 2,
BeatmapInfo = beatmap,
BeatmapHash = beatmap.Hash
});
});
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]
public void TestScoreLostOnModification()
{

View File

@ -170,7 +170,16 @@ namespace osu.Game.Tests.Visual.Gameplay
ManualClock clock = null;
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()));
@ -209,6 +218,49 @@ namespace osu.Game.Tests.Visual.Gameplay
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)
{
AddStep("create test", () =>
@ -289,7 +341,7 @@ namespace osu.Game.Tests.Visual.Gameplay
RegisterPool<TestHitObject, DrawableTestHitObject>(poolSize);
RegisterPool<TestKilledHitObject, DrawableTestKilledHitObject>(poolSize);
RegisterPool<TestHitObjectWithNested, DrawableTestHitObjectWithNested>(poolSize);
RegisterPool<NestedHitObject, DrawableNestedHitObject>(poolSize);
RegisterPool<PooledNestedHitObject, DrawableNestedHitObject>(poolSize);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject);
@ -422,16 +474,22 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestHitObjectWithNested : TestHitObject
{
public IEnumerable<HitObject> NestedObjects { get; init; } = Array.Empty<HitObject>();
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
base.CreateNestedHitObjects(cancellationToken);
for (int i = 0; i < 3; ++i)
AddNested(new NestedHitObject { StartTime = (float)Duration * (i + 1) / 4 });
foreach (var ho in NestedObjects)
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);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
=> hitObject is NonPooledNestedHitObject nonPooled ? new DrawableNestedHitObject(nonPooled) : null;
protected override void CheckForResult(bool userTriggered, double 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()
: this(null)
{
}
public DrawableNestedHitObject(NestedHitObject hitObject)
public DrawableNestedHitObject(PooledNestedHitObject hitObject)
: base(hitObject)
{
}
public DrawableNestedHitObject(NonPooledNestedHitObject hitObject)
: base(hitObject)
{
Size = new Vector2(15);
Colour = Colour4.White;
RelativePositionAxes = Axes.Both;
Origin = Anchor.Centre;
}
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(15);
Colour = Colour4.White;
RelativePositionAxes = Axes.Both;
Origin = Anchor.Centre;
AddInternal(new Circle
{
RelativeSizeAxes = Axes.Both,

View File

@ -107,6 +107,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestBeatmapDownloadingStates()
{
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
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);
AddStep("set beatmap available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
}
private void checkProgressBarVisibility(bool visible) =>

View File

@ -20,6 +20,7 @@ using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms;
namespace osu.Game.Beatmaps
@ -204,6 +205,15 @@ namespace osu.Game.Beatmaps
protected override void PostImport(BeatmapSetInfo model, Realm realm, ImportParameters 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);
}

View File

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

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers
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;
/// <summary>
@ -139,6 +139,8 @@ namespace osu.Game.Graphics.Containers
private readonly Box box;
protected override float MinimumDimSize => SCROLL_BAR_WIDTH * 3;
public OsuScrollbar(Direction scrollDir)
: base(scrollDir)
{
@ -147,7 +149,7 @@ namespace osu.Game.Graphics.Containers
CornerRadius = 5;
// 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;
@ -173,11 +175,10 @@ namespace osu.Game.Graphics.Containers
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
};
this.ResizeTo(size, duration, easing);
}, duration, easing);
}
protected override bool OnHover(HoverEvent e)

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary>
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>
/// "Account"
/// </summary>

View File

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

View File

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

View File

@ -60,6 +60,15 @@ namespace osu.Game.Online.Rooms
if (item.NewValue == null)
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();
selectedBeatmap = null;
@ -115,6 +124,9 @@ namespace osu.Game.Online.Rooms
switch (downloadTracker.State.Value)
{
case DownloadState.Unknown:
availability.Value = BeatmapAvailability.Unknown();
break;
case DownloadState.NotDownloaded:
availability.Value = BeatmapAvailability.NotDownloaded();
break;

View File

@ -1164,7 +1164,9 @@ namespace osu.Game
private void forwardTabletLogsToNotifications()
{
const string tablet_prefix = @"[Tablet] ";
bool notifyOnWarning = true;
bool notifyOnError = true;
Logger.NewEntry += entry =>
{
@ -1175,6 +1177,11 @@ namespace osu.Game
if (entry.Level == LogLevel.Error)
{
if (!notifyOnError)
return;
notifyOnError = false;
Schedule(() =>
{
Notifications.Post(new SimpleNotification
@ -1213,7 +1220,11 @@ namespace osu.Game
Schedule(() =>
{
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>
/// All ongoing operations (ie. any <see cref="ProgressNotification"/> not in a completed state).
/// </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))]
AppearOffline,
[LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.SignOut))]
SignOut,
}
}

View File

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

View File

@ -98,9 +98,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
public virtual bool DisplayResult => true;
/// <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>
public bool AllJudged => Judged && NestedHitObjects.All(h => h.AllJudged);
public JudgementResult Result => Entry?.Result;
/// <summary>
/// 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.
/// Note: This does NOT include nested hitobjects.
/// </summary>
public bool Judged => Result?.HasResult ?? true;
public bool Judged => Entry?.Judged ?? true;
/// <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>
public JudgementResult Result => Entry?.Result;
public bool AllJudged => Entry?.AllJudged ?? true;
/// <summary>
/// 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)
{
Debug.Assert(Entry != null);
// LifetimeStart is already computed using HitObjectLifetimeEntry's InitialLifetimeOffset.
// We override this with DHO's InitialLifetimeOffset for a non-pooled DHO.
if (entry is SyntheticHitObjectEntry)
@ -247,6 +249,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
drawableNested.ParentHitObject = this;
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);
}
@ -290,6 +298,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
{
Debug.Assert(Entry != null);
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
if (HitObject is IHasComboInformation combo)
@ -318,6 +328,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
nestedHitObjects.Clear();
// clean up synthetic entries manually added in `Apply()`.
Entry.NestedEntries.RemoveAll(nestedEntry => nestedEntry is SyntheticHitObjectEntry);
ClearNestedHitObjects();
HitObject.DefaultsApplied -= onDefaultsApplied;

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Performance;
using osu.Game.Rulesets.Judgements;
@ -19,12 +21,28 @@ namespace osu.Game.Rulesets.Objects
/// </summary>
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>
/// The result that <see cref="HitObject"/> was judged with.
/// This is set by the accompanying <see cref="DrawableHitObject"/>, and reused when required for rewinding.
/// </summary>
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();
internal event Action? RevertResult;

View File

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

View File

@ -82,6 +82,7 @@ namespace osu.Game.Scoring
{
Ruleset = ruleset ?? new RulesetInfo();
BeatmapInfo = beatmap ?? new BeatmapInfo();
BeatmapHash = BeatmapInfo.Hash;
RealmUser = realmUser ?? new RealmUser();
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 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.Origin = Anchor.BottomCentre;
buttons.Y = Math.Min(0, ToLocalSpace(Parent.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
}
else if (topExcess > bottomExcess)
{

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
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.AutoSizeAxes = Axes.X;

View File

@ -313,16 +313,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
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.
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
else if (client.LocalUser?.State == MultiplayerUserState.Spectating
&& (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
{
onLoadRequested();
case DownloadState.LocallyAvailable:
if (client.LocalUser?.State == MultiplayerUserState.Spectating
&& (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);
break;
case DownloadState.Unknown:
text.Text = "checking availability";
icon.Icon = FontAwesome.Solid.Question;
icon.Colour = colours.Orange0;
break;
case DownloadState.NotDownloaded:
text.Text = "no map";
icon.Icon = FontAwesome.Solid.MinusCircle;