1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 03:02:54 +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>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(200); seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
} }
[Test] [Test]
@ -100,13 +100,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(200); seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
} }
[Test] [Test]
@ -145,23 +145,23 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(120); seekTo(120);
AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first)); AddAssert("most valid object is first hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(first));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_NORMAL);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_NORMAL);
seekTo(480); seekTo(480);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(700); seekTo(700);
AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second)); AddAssert("most valid object is second hit", () => triggerSource.GetMostValidObject(), () => Is.EqualTo(second));
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
} }
[Test] [Test]
@ -174,8 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
StartTime = 100, StartTime = 100,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
} }
}; };
hit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); 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>); AddAssert("most valid object is nested strong hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit.StrongNestedHit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(200); seekTo(200);
AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>); AddAssert("most valid object is hit", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Hit>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
} }
[Test] [Test]
@ -213,18 +213,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600); seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200); seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
} }
[Test] [Test]
@ -247,18 +247,18 @@ namespace osu.Game.Rulesets.Taiko.Tests
}); });
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(600); seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
seekTo(1200); seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_SOFT);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_SOFT);
} }
[Test] [Test]
@ -272,8 +272,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100, EndTime = 1100,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum"), new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM),
new HitSampleInfo(HitSampleInfo.HIT_FINISH, "drum") // implies strong new HitSampleInfo(HitSampleInfo.HIT_FINISH, HitSampleInfo.BANK_DRUM) // implies strong
} }
}; };
drumRoll.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); 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>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(600); seekTo(600);
AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>); AddAssert("most valid object is drum roll tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRollTick>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
seekTo(1200); seekTo(1200);
AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>); AddAssert("most valid object is drum roll", () => triggerSource.GetMostValidObject(), Is.InstanceOf<DrumRoll>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, $"{HitSampleInfo.HIT_NORMAL},{HitSampleInfo.HIT_FINISH}", HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, $"{HitSampleInfo.HIT_CLAP},{HitSampleInfo.HIT_WHISTLE}", HitSampleInfo.BANK_DRUM);
} }
[Test] [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. // 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. // But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(600); seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
seekTo(1200); seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
} }
[Test] [Test]
@ -344,7 +344,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
EndTime = 1100, EndTime = 1100,
Samples = new List<HitSampleInfo> Samples = new List<HitSampleInfo>
{ {
new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "drum") new HitSampleInfo(HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM)
} }
}; };
swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); 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. // 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. // But for sample playback purposes they can be ignored as noise.
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(600); seekTo(600);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "drum"); checkSamples(HitType.Rim, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
seekTo(1200); seekTo(1200);
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>); AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
checkSound(HitType.Centre, HitSampleInfo.HIT_NORMAL, "drum"); checkSamples(HitType.Centre, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
checkSound(HitType.Rim, HitSampleInfo.HIT_CLAP, "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)); 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 is {expectedSamplesCsv}", () => string.Join(',', triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Select(s => s.Name)),
AddAssert($"last played sample has {expectedBank} bank", () => triggerSource.LastPlayedSamples!.OfType<HitSampleInfo>().Single().Bank, () => Is.EqualTo(expectedBank)); () => 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)); private void seekTo(double time) => AddStep($"seek to {time}", () => gameplayClock.Seek(time));

View File

@ -3,15 +3,16 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
namespace osu.Game.Rulesets.Taiko.Tests namespace osu.Game.Rulesets.Taiko.Tests
{ {
/// <summary> /// <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> /// </summary>
[HeadlessTest] [HeadlessTest]
public partial class TestSceneSampleOutput : TestSceneTaikoPlayer public partial class TestSceneSampleOutput : TestSceneTaikoPlayer
@ -26,10 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests
string.Empty, string.Empty,
string.Empty, string.Empty,
string.Empty, string.Empty,
HitSampleInfo.HIT_FINISH, string.Empty,
HitSampleInfo.HIT_WHISTLE, string.Empty,
HitSampleInfo.HIT_WHISTLE, string.Empty,
HitSampleInfo.HIT_WHISTLE, string.Empty,
}; };
var actualSampleNames = new List<string>(); var actualSampleNames = new List<string>();
@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); 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"); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions");

View File

@ -4,14 +4,12 @@
#nullable disable #nullable disable
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Skinning.Default; 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.CentreHit), _ => new CentreHitCirclePiece(), confineMode: ConfineMode.ScaleToFit)
: new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.RimHit), _ => new RimHitCirclePiece(), 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) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
Debug.Assert(HitObject.HitWindows != null); Debug.Assert(HitObject.HitWindows != null);

View File

@ -119,8 +119,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool RemoveWhenNotAlive => false; public override bool RemoveWhenNotAlive => false;
} }
// Most osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource). // osu!taiko hitsounds are managed by the drum (see DrumSampleTriggerSource).
public override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>(); public sealed override IEnumerable<HitSampleInfo> GetSamples() => Enumerable.Empty<HitSampleInfo>();
} }
public abstract partial class DrawableTaikoHitObject<TObject> : DrawableTaikoHitObject 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -18,12 +17,25 @@ namespace osu.Game.Rulesets.Taiko.UI
public void Play(HitType hitType) 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; 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"); 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.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. realm.Add(new ScoreInfo
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo {
// { OnlineID = 2,
// OnlineID = 2, BeatmapInfo = beatmap,
// Beatmap = beatmap, BeatmapHash = beatmap.Hash
// BeatmapInfoID = beatmap.ID });
// }, 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

@ -133,6 +133,32 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("objects reverted to original position", () => addedObjects[0].Position == new Vector2(0)); 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] [Test]
public void TestBasicSelect() public void TestBasicSelect()
{ {

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

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

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

@ -3,10 +3,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using AutoMapper; using AutoMapper;
using AutoMapper.Internal; using AutoMapper.Internal;
using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Models; using osu.Game.Models;
@ -52,10 +54,23 @@ namespace osu.Game.Database
{ {
foreach (var beatmap in s.Beatmaps) 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) if (existingBeatmap != null)
copyChangesToRealm(beatmap, existing); {
// 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 else
{ {
var newBeatmap = new BeatmapInfo var newBeatmap = new BeatmapInfo
@ -64,6 +79,7 @@ namespace osu.Game.Database
BeatmapSet = d, BeatmapSet = d,
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName) Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)
}; };
d.Beatmaps.Add(newBeatmap); d.Beatmaps.Add(newBeatmap);
copyChangesToRealm(beatmap, 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 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

@ -88,6 +88,16 @@ Please try changing your audio device to a working setting.");
/// </summary> /// </summary>
public static LocalisableString LinkTypeNotSupported => new TranslatableString(getKey(@"unsupported_link_type"), @"This link type is not yet supported!"); 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}"; 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.Framework.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -154,7 +155,7 @@ namespace osu.Game.Online.Chat
: base(message, channel) : base(message, channel)
{ {
Icon = FontAwesome.Solid.Envelope; 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) : base(message, channel)
{ {
Icon = FontAwesome.Solid.At; 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() 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

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

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)
{
foreach (var childEntry in childEntries)
Remove(childEntry); 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

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

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;