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

Merge branch 'master' into availability-fixes

This commit is contained in:
Bartłomiej Dach 2023-07-02 20:38:37 +02:00 committed by Dean Herbert
commit 4d9c3091a1
29 changed files with 456 additions and 180 deletions

View File

@ -72,13 +72,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(120);
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(480);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(700);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = 100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
}
};
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -184,13 +184,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
}
[Test]
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
}
[Test]
@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
}
};
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -282,18 +282,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
});
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
}
[Test]
@ -319,18 +319,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
}
[Test]
@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100,
Samples = new List<HitSampleInfo>
{
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum")
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM)
}
};
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
@ -356,25 +356,26 @@ namespace osu.Game.Rulesets.Taiko.Tests
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
// But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum");
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum");
checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
}
private void checkSound(HitType hitType, string expectedName, string expectedBank)
private void checkSamples(HitType hitType, string expectedSamplesCsv, string expectedBank)
{
AddStep($"hit {hitType}", () => triggerSource.Play(hitType));
AddAssert($"last played sample is {expectedName}", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Name, () => Is.EqualTo(expectedName));
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank));
AddAssert($"last played sample is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
() => Is.EqualTo(expectedSamplesCsv));
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().First().Bank, () => Is.EqualTo(expectedBank));
}
private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));

View File

@ -3,15 +3,16 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests
{
/// <summary>
/// Taiko has some interesting rules for legacy mappings.
/// Taiko doesn't output any samples. They are all handled externally by <see cref="DrumSamplePlayer"/>.
/// </summary>
[HeadlessTest]
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
string.Empty,
string.Empty,
string.Empty,
HitSampleInfo.HIT_FINISH,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
HitSampleInfo.HIT_WHISTLE,
string.Empty,
string.Empty,
string.Empty,
string.Empty,
};
var actualSampleNames = new List<string>();
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length);
AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames));
AddAssert("samples are correct", () => actualSampleNames, () => Is.EqualTo(expectedSampleNames));
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");

View File

@ -4,14 +4,12 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Default;
@ -93,40 +91,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
? new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), confineMode: ConfineMode.ScaleToFit);
public override IEnumerable<HitSampleInfo> GetSamples()
{
// normal and claps are always handled by the drum (see DrumSampleMapping).
// in addition, whistles are excluded as they are an alternative rim marker.
var samples = HitObject.Samples.Where(s =>
s.Name != HitSampleInfo.HIT_NORMAL
&& s.Name != HitSampleInfo.HIT_CLAP
&& s.Name != HitSampleInfo.HIT_WHISTLE);
if (HitObject.Type == HitType.Rim && HitObject.IsStrong)
{
// strong + rim always maps to whistle.
// TODO: this should really be in the legacy decoder, but can't be because legacy encoding parity would be broken.
// when we add a taiko editor, this is probably not going to play nice.
var corrected = samples.ToList();
for (int i = 0; i < corrected.Count; i++)
{
var s = corrected[i];
if (s.Name != HitSampleInfo.HIT_FINISH)
continue;
corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE);
}
return corrected;
}
return samples;
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
{
Debug.Assert(HitObject.HitWindows != null);

View File

@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool RemoveWhenNotAlive => false;
}
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
// osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
public sealed override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
}
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI;
@ -18,12 +17,25 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType)
{
var hitSample = GetMostValidObject()?.Samples?.FirstOrDefault(o => o.Name == HitSampleInfo.HIT_NORMAL);
TaikoHitObject? hitObject = GetMostValidObject() as TaikoHitObject;
if (hitSample == null)
if (hitObject == null)
return;
PlaySamples(new ISampleInfo[] { new HitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL, hitSample.Bank, volume: hitSample.Volume) });
var baseSample = hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_CLAP : HitSampleInfo.HIT_NORMAL);
if ((hitObject as TaikoStrongableHitObject)?.IsStrong == true || hitObject is StrongNestedHitObject)
{
PlaySamples(new ISampleInfo[]
{
baseSample,
hitObject.CreateHitSampleInfo(hitType == HitType.Rim ? HitSampleInfo.HIT_WHISTLE : HitSampleInfo.HIT_FINISH)
});
}
else
{
PlaySamples(new ISampleInfo[] { baseSample });
}
}
public override void Play() => throw new InvalidOperationException(@"Use override with HitType parameter instead");

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

@ -133,6 +133,32 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("objects reverted to original position", () => addedObjects[0].Position == new Vector2(0));
}
[Test]
public void TestGlobalFlipHotkeys()
{
HitCircle addedObject = null;
AddStep("add hitobjects", () => EditorBeatmap.Add(addedObject = new HitCircle { StartTime = 100 }));
AddStep("select objects", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("flip horizontally across playfield", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.H);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("objects flipped horizontally", () => addedObject.Position == new Vector2(OsuPlayfield.BASE_SIZE.X, 0));
AddStep("flip vertically across playfield", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.J);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("objects flipped vertically", () => addedObject.Position == OsuPlayfield.BASE_SIZE);
}
[Test]
public void TestBasicSelect()
{

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

@ -188,7 +188,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[]
{
new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.##}{srExtra}"))
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}"))
}
},
new FillFlowContainer

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

@ -3,10 +3,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.Serialization;
using AutoMapper;
using AutoMapper.Internal;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Models;
@ -52,10 +54,23 @@ namespace osu.Game.Database
{
foreach (var beatmap in s.Beatmaps)
{
var existing = d.Beatmaps.FirstOrDefault(b => b.ID == beatmap.ID);
// Importantly, search all of realm for the beatmap (not just the set's beatmaps).
// It may have gotten detached, and if that's the case let's use this opportunity to fix
// things up.
var existingBeatmap = d.Realm.Find<BeatmapInfo>(beatmap.ID);
if (existing != null)
copyChangesToRealm(beatmap, existing);
if (existingBeatmap != null)
{
// As above, reattach if it happens to not be in the set's beatmaps.
if (!d.Beatmaps.Contains(existingBeatmap))
{
Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further.");
Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important);
d.Beatmaps.Add(existingBeatmap);
}
copyChangesToRealm(beatmap, existingBeatmap);
}
else
{
var newBeatmap = new BeatmapInfo
@ -64,6 +79,7 @@ namespace osu.Game.Database
BeatmapSet = d,
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)
};
d.Beatmaps.Add(newBeatmap);
copyChangesToRealm(beatmap, newBeatmap);
}

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

@ -88,6 +88,16 @@ Please try changing your audio device to a working setting.");
/// </summary>
public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!");
/// <summary>
/// "You received a private message from '{0}'. Click to read it!"
/// </summary>
public static LocalisableString PrivateMessageReceived(string username) => new TranslatableString(getKey(@"private_message_received"), @"You received a private message from '{0}'. Click to read it!", username);
/// <summary>
/// "Your name was mentioned in chat by '{0}'. Click to find out why!"
/// </summary>
public static LocalisableString YourNameWasMentioned(string username) => new TranslatableString(getKey(@"your_name_was_mentioned"), @"Your name was mentioned in chat by '{0}'. Click to find out why!", username);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@ -154,7 +155,7 @@ namespace osu.Game.Online.Chat
: base(message, channel)
{
Icon = FontAwesome.Solid.Envelope;
Text = $"You received a private message from '{message.Sender.Username}'. Click to read it!";
Text = NotificationsStrings.PrivateMessageReceived(message.Sender.Username);
}
}
@ -164,7 +165,7 @@ namespace osu.Game.Online.Chat
: base(message, channel)
{
Icon = FontAwesome.Solid.At;
Text = $"Your name was mentioned in chat by '{message.Sender.Username}'. Click to find out why!";
Text = NotificationsStrings.YourNameWasMentioned(message.Sender.Username);
}
}

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

@ -185,7 +185,7 @@ namespace osu.Game.Overlays.BeatmapSet
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.##");
starRating.Text = beatmap.StarRating.ToLocalisableString(@"0.00");
starRatingContainer.FadeIn(100);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },

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

@ -160,13 +160,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (e.Repeat)
return false;
bool handled;
switch (e.Action)
{
case GlobalAction.EditorFlipHorizontally:
return HandleFlip(Direction.Horizontal, true);
ChangeHandler?.BeginChange();
handled = HandleFlip(Direction.Horizontal, true);
ChangeHandler?.EndChange();
return handled;
case GlobalAction.EditorFlipVertically:
return HandleFlip(Direction.Vertical, true);
ChangeHandler?.BeginChange();
handled = HandleFlip(Direction.Vertical, true);
ChangeHandler?.EndChange();
return handled;
}
return false;

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;