1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 14:02:55 +08:00

Merge branch 'master' into chat-mention-highlight

This commit is contained in:
Dean Herbert 2022-03-08 21:15:12 +09:00
commit da7c6f1772
30 changed files with 658 additions and 143 deletions

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -96,6 +97,8 @@ namespace osu.Desktop
switch (RuntimeInfo.OS) switch (RuntimeInfo.OS)
{ {
case RuntimeInfo.Platform.Windows: case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
return new SquirrelUpdateManager(); return new SquirrelUpdateManager();
default: default:

View File

@ -3,6 +3,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Runtime.Versioning;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Desktop.LegacyIpc; using osu.Desktop.LegacyIpc;
@ -12,6 +13,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using Squirrel;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -24,6 +26,10 @@ namespace osu.Desktop
[STAThread] [STAThread]
public static void Main(string[] args) public static void Main(string[] args)
{ {
// run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows())
setupSquirrel();
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory; string cwd = Environment.CurrentDirectory;
@ -104,6 +110,23 @@ namespace osu.Desktop
} }
} }
[SupportedOSPlatform("windows")]
private static void setupSquirrel()
{
SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) =>
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
}, onAppUninstall: (version, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) =>
{
tools.SetProcessAppUserModelId();
});
}
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1; private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary> /// <summary>

View File

@ -2,6 +2,7 @@
// 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.Runtime.Versioning;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -16,10 +17,11 @@ using osu.Game.Overlays.Notifications;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using Squirrel; using Squirrel;
using LogLevel = Splat.LogLevel; using Squirrel.SimpleSplat;
namespace osu.Desktop.Updater namespace osu.Desktop.Updater
{ {
[SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{ {
private UpdateManager updateManager; private UpdateManager updateManager;
@ -34,12 +36,14 @@ namespace osu.Desktop.Updater
/// </summary> /// </summary>
private bool updatePending; private bool updatePending;
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(NotificationOverlay notification) private void load(NotificationOverlay notification)
{ {
notificationOverlay = notification; notificationOverlay = notification;
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
} }
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
@ -49,9 +53,11 @@ namespace osu.Desktop.Updater
// should we schedule a retry on completion of this check? // should we schedule a retry on completion of this check?
bool scheduleRecheck = true; bool scheduleRecheck = true;
const string github_token = null; // TODO: populate.
try try
{ {
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false); updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
@ -201,11 +207,11 @@ namespace osu.Desktop.Updater
} }
} }
private class SquirrelLogger : Splat.ILogger, IDisposable private class SquirrelLogger : ILogger, IDisposable
{ {
public LogLevel Level { get; set; } = LogLevel.Info; public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
public void Write(string message, LogLevel logLevel) public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
{ {
if (logLevel < Level) if (logLevel < Level)
return; return;

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"> <asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" name="osu!" /> <assemblyIdentity version="1.0.0.0" name="osu!" />
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security> <security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">

View File

@ -24,10 +24,10 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.15-pre" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" /> <PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" /> <PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" /> <PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.14"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.14">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -1,25 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using Realms; using Realms;
#nullable enable
namespace osu.Game.Tests.Database namespace osu.Game.Tests.Database
{ {
[TestFixture] [TestFixture]
public class RealmSubscriptionRegistrationTests : RealmTest public class RealmSubscriptionRegistrationTests : RealmTest
{ {
[Test]
public void TestSubscriptionCollectionAndPropertyChanges()
{
int collectionChanges = 0;
int propertyChanges = 0;
ChangeSet? lastChanges = null;
RunTestWithRealm((realm, _) =>
{
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
realm.Run(r => r.Refresh());
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1));
Assert.That(propertyChanges, Is.EqualTo(0));
Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5);
realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1));
Assert.That(propertyChanges, Is.EqualTo(1));
Assert.That(lastChanges?.InsertedIndices, Is.Empty);
Assert.That(lastChanges?.ModifiedIndices, Has.One.Items);
Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items);
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
{
lastChanges = changes;
if (changes == null)
return;
if (changes.HasCollectionChanges())
{
Interlocked.Increment(ref collectionChanges);
}
else
{
Interlocked.Increment(ref propertyChanges);
}
}
}
[Test] [Test]
public void TestSubscriptionWithAsyncWrite() public void TestSubscriptionWithAsyncWrite()
{ {
@ -47,6 +102,28 @@ namespace osu.Game.Tests.Database
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes; void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
} }
[Test]
public void TestPropertyChangedSubscription()
{
RunTestWithRealm((realm, _) =>
{
bool? receivedValue = null;
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
using (realm.SubscribeToPropertyChanged(r => r.All<BeatmapSetInfo>().First(), setInfo => setInfo.Protected, val => receivedValue = val))
{
Assert.That(receivedValue, Is.False);
realm.Write(r => r.All<BeatmapSetInfo>().First().Protected = true);
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.True);
}
});
}
[Test] [Test]
public void TestSubscriptionWithContextLoss() public void TestSubscriptionWithContextLoss()
{ {
@ -163,5 +240,41 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Null); Assert.That(beatmapSetInfo, Is.Null);
}); });
} }
[Test]
public void TestPropertyChangedSubscriptionWithContextLoss()
{
RunTestWithRealm((realm, _) =>
{
bool? receivedValue = null;
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
var subscription = realm.SubscribeToPropertyChanged(
r => r.All<BeatmapSetInfo>().First(),
setInfo => setInfo.Protected,
val => receivedValue = val);
Assert.That(receivedValue, Is.Not.Null);
receivedValue = null;
using (realm.BlockAllOperations())
{
}
// re-registration after context restore.
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.Not.Null);
subscription.Dispose();
receivedValue = null;
using (realm.BlockAllOperations())
Assert.That(receivedValue, Is.Null);
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.Null);
});
}
} }
} }

View File

@ -1,29 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -118,59 +112,6 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue); AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
} }
[TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(ModWindUp), 1.5)]
[TestCase(typeof(ModWindDown), 0.75)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
[TestCase(typeof(ModWindUp), 2)]
[TestCase(typeof(ModWindDown), 0.5)]
public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
{
GameplayClockContainer gameplayContainer = null;
StoryboardSampleInfo sampleInfo = null;
TestDrawableStoryboardSample sample = null;
Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
switch (testedMod)
{
case ModRateAdjust m:
m.SpeedChange.Value = expectedRate;
break;
case ModTimeRamp m:
m.FinalRate.Value = m.InitialRate.Value = expectedRate;
break;
}
AddStep("setup storyboard sample", () =>
{
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this);
SelectedMods.Value = new[] { testedMod };
var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
Child = beatmapSkinSourceContainer
});
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
});
AddStep("start", () => gameplayContainer.Start());
AddAssert("sample playback rate matches mod rates", () =>
testedMod != null && Precision.AlmostEquals(
sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value,
((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime)));
}
[Test] [Test]
public void TestSamplePlaybackWithBeatmapHitsoundsOff() public void TestSamplePlaybackWithBeatmapHitsoundsOff()
{ {

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using NUnit.Framework;
using osu.Game.IO.Serialization;
using osu.Game.Online.Solo;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Online
{
/// <summary>
/// Basic testing to ensure our attribute-based naming is correctly working.
/// </summary>
[TestFixture]
public class TestSubmittableScoreJsonSerialization
{
[Test]
public void TestScoreSerialisationViaExtensionMethod()
{
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
string serialised = score.Serialize();
Assert.That(serialised, Contains.Substring("large_tick_hit"));
Assert.That(serialised, Contains.Substring("\"rank\": \"S\""));
}
[Test]
public void TestScoreSerialisationWithoutSettings()
{
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
string serialised = JsonConvert.SerializeObject(score);
Assert.That(serialised, Contains.Substring("large_tick_hit"));
Assert.That(serialised, Contains.Substring("\"rank\":\"S\""));
}
}
}

View File

@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
storyboardContainer.Clock = decoupledClock; storyboardContainer.Clock = decoupledClock;
storyboard = working.Storyboard.CreateDrawable(Beatmap.Value); storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value);
storyboard.Passing = false; storyboard.Passing = false;
storyboardContainer.Add(storyboard); storyboardContainer.Add(storyboard);
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sb = decoder.Decode(bfr); sb = decoder.Decode(bfr);
} }
storyboard = sb.CreateDrawable(Beatmap.Value); storyboard = sb.CreateDrawable(SelectedMods.Value);
storyboardContainer.Add(storyboard); storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(Beatmap.Value.Track); decoupledClock.ChangeSource(Beatmap.Value.Track);

View File

@ -1,17 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Audio;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -19,6 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private Storyboard storyboard; private Storyboard storyboard;
private IReadOnlyList<Mod> storyboardMods;
protected override bool HasCustomSteps => true;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
@ -31,42 +41,107 @@ namespace osu.Game.Tests.Visual.Gameplay
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
} }
[SetUp]
public void SetUp() => storyboardMods = Array.Empty<Mod>();
[Test] [Test]
public void TestStoryboardSamplesStopDuringPause() public void TestStoryboardSamplesStopDuringPause()
{ {
checkForFirstSamplePlayback(); createPlayerTest();
AddStep("player paused", () => Player.Pause()); AddStep("player paused", () => Player.Pause());
AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value);
AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); allStoryboardSamplesStopped();
AddStep("player resume", () => Player.Resume()); AddStep("player resume", () => Player.Resume());
AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); waitUntilStoryboardSamplesPlay();
} }
[Test] [Test]
public void TestStoryboardSamplesStopOnSkip() public void TestStoryboardSamplesStopOnSkip()
{ {
checkForFirstSamplePlayback(); createPlayerTest();
AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space)); skipIntro();
AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); allStoryboardSamplesStopped();
AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); waitUntilStoryboardSamplesPlay();
} }
private void checkForFirstSamplePlayback() [TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
public void TestStoryboardSamplesPlaybackWithRateAdjustMods(Type expectedMod, double expectedRate)
{ {
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null); AddStep("setup mod", () =>
AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); {
ModRateAdjust testedMod = (ModRateAdjust)Activator.CreateInstance(expectedMod).AsNonNull();
testedMod.SpeedChange.Value = expectedRate;
storyboardMods = new[] { testedMod };
});
createPlayerTest();
skipIntro();
AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate));
} }
[TestCase(typeof(ModWindUp), 0.5, 2)]
[TestCase(typeof(ModWindUp), 1.51, 2)]
[TestCase(typeof(ModWindDown), 2, 0.5)]
[TestCase(typeof(ModWindDown), 0.99, 0.5)]
public void TestStoryboardSamplesPlaybackWithTimeRampMods(Type expectedMod, double initialRate, double finalRate)
{
AddStep("setup mod", () =>
{
ModTimeRamp testedMod = (ModTimeRamp)Activator.CreateInstance(expectedMod).AsNonNull();
testedMod.InitialRate.Value = initialRate;
testedMod.FinalRate.Value = finalRate;
storyboardMods = new[] { testedMod };
});
createPlayerTest();
skipIntro();
ModTimeRamp gameplayMod = null;
AddUntilStep("mod speed change updated", () =>
{
gameplayMod = Player.GameplayState.Mods.OfType<ModTimeRamp>().Single();
return gameplayMod.SpeedChange.Value != initialRate;
});
AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value));
}
private void createPlayerTest()
{
CreateTest(null);
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
waitUntilStoryboardSamplesPlay();
}
private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
private void allStoryboardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space));
private IEnumerable<DrawableStoryboardSample> allStoryboardSamples => Player.ChildrenOfType<DrawableStoryboardSample>(); private IEnumerable<DrawableStoryboardSample> allStoryboardSamples => Player.ChildrenOfType<DrawableStoryboardSample>();
protected override bool AllowFail => false; protected override bool AllowFail => false;
protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(storyboardMods).ToArray();
return new TestPlayer(true, false);
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio); new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio);

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestScenePopupScreenTitle : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestPopupScreenTitle()
{
AddStep("create content", () =>
{
Child = new PopupScreenTitle
{
Title = "Popup Screen Title",
Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)),
Close = () => { }
};
});
}
[Test]
public void TestDisabledExit()
{
AddStep("create content", () =>
{
Child = new PopupScreenTitle
{
Title = "Popup Screen Title",
Description = "This is a description."
};
});
}
}
}

View File

@ -3,9 +3,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -318,6 +320,66 @@ namespace osu.Game.Database
} }
} }
/// <summary>
/// Subscribe to the property of a realm object to watch for changes.
/// </summary>
/// <remarks>
/// On subscribing, unless the <paramref name="modelAccessor"/> does not match an object, an initial invocation of <paramref name="onChanged"/> will occur immediately.
/// Further invocations will occur when the value changes, but may also fire on a realm recycle with no actual value change.
/// </remarks>
/// <param name="modelAccessor">A function to retrieve the relevant model from realm.</param>
/// <param name="propertyLookup">A function to traverse to the relevant property from the model.</param>
/// <param name="onChanged">A function to be invoked when a change of value occurs.</param>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TProperty">The type of the property to be watched.</typeparam>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
/// </returns>
public IDisposable SubscribeToPropertyChanged<TModel, TProperty>(Func<Realm, TModel?> modelAccessor, Expression<Func<TModel, TProperty>> propertyLookup, Action<TProperty> onChanged)
where TModel : RealmObjectBase
{
return RegisterCustomSubscription(r =>
{
string propertyName = getMemberName(propertyLookup);
var model = Run(modelAccessor);
var propLookupCompiled = propertyLookup.Compile();
if (model == null)
return null;
model.PropertyChanged += onPropertyChanged;
// Update initial value immediately.
onChanged(propLookupCompiled(model));
return new InvokeOnDisposal(() => model.PropertyChanged -= onPropertyChanged);
void onPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == propertyName)
onChanged(propLookupCompiled(model));
}
});
static string getMemberName(Expression<Func<TModel, TProperty>> expression)
{
if (!(expression is LambdaExpression lambda))
throw new ArgumentException("Outermost expression must be a lambda expression", nameof(expression));
if (!(lambda.Body is MemberExpression memberExpression))
throw new ArgumentException("Lambda body must be a member access expression", nameof(expression));
// TODO: nested access can be supported, with more iteration here
// (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit)
if (memberExpression.Expression != lambda.Parameters[0])
throw new ArgumentException("Nested access expressions are not supported", nameof(expression));
return memberExpression.Member.Name;
}
}
/// <summary> /// <summary>
/// Run work on realm that will be run every time the update thread realm instance gets recycled. /// Run work on realm that will be run every time the update thread realm instance gets recycled.
/// </summary> /// </summary>

View File

@ -4,6 +4,8 @@
using System; using System;
using Realms; using Realms;
#nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
public static class RealmExtensions public static class RealmExtensions
@ -22,5 +24,14 @@ namespace osu.Game.Database
transaction.Commit(); transaction.Commit();
return result; return result;
} }
/// <summary>
/// Whether the provided change set has changes to the top level collection.
/// </summary>
/// <remarks>
/// Realm subscriptions fire on both collection and property changes (including *all* nested properties).
/// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
/// </remarks>
public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
} }
} }

View File

@ -3,12 +3,15 @@
#nullable enable #nullable enable
using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
namespace osu.Game.Graphics.Backgrounds namespace osu.Game.Graphics.Backgrounds
@ -20,6 +23,9 @@ namespace osu.Game.Graphics.Backgrounds
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private MusicController? musicController { get; set; } private MusicController? musicController { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1") public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1")
: base(beatmap, fallbackTextureName) : base(beatmap, fallbackTextureName)
{ {
@ -39,7 +45,7 @@ namespace osu.Game.Graphics.Backgrounds
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 }, Volume = { Value = 0 },
Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock } Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock }
}, AddInternal); }, AddInternal);
} }

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public class PopupScreenTitle : CompositeDrawable
{
public LocalisableString Title
{
set => titleSpriteText.Text = value;
}
public LocalisableString Description
{
set => descriptionText.Text = value;
}
public Action? Close
{
get => closeButton.Action;
set => closeButton.Action = value;
}
private const float corner_radius = 14;
private const float main_area_height = 70;
private readonly Container underlayContainer;
private readonly Box underlayBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private readonly OsuSpriteText titleSpriteText;
private readonly OsuTextFlowContainer descriptionText;
private readonly IconButton closeButton;
public PopupScreenTitle()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = 70,
Top = -corner_radius
},
Children = new Drawable[]
{
underlayContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = main_area_height + 2 * corner_radius,
CornerRadius = corner_radius,
Masking = true,
BorderThickness = 2,
Child = underlayBackground = new Box
{
RelativeSizeAxes = Axes.Both
}
},
contentContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = main_area_height + corner_radius,
CornerRadius = corner_radius,
Masking = true,
BorderThickness = 2,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.Black.Opacity(0.1f),
Offset = new Vector2(0, 1),
Radius = 3
},
Children = new Drawable[]
{
contentBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Top = corner_radius },
Padding = new MarginPadding { Horizontal = 100 },
Children = new Drawable[]
{
titleSpriteText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 20)
},
descriptionText = new OsuTextFlowContainer(t =>
{
t.Font = OsuFont.Default.With(size: 12);
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
closeButton = new IconButton
{
Icon = FontAwesome.Solid.Times,
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding
{
Right = 21,
Top = corner_radius
}
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
underlayContainer.BorderColour = ColourInfo.GradientVertical(Colour4.Black, colourProvider.Dark4);
underlayBackground.Colour = colourProvider.Dark4;
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Dark3, colourProvider.Dark1);
contentBackground.Colour = colourProvider.Dark3;
closeButton.IconHoverColour = colourProvider.Highlight1;
}
}
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.Text = value; set => Component.Text = value;
} }
public Container TabbableContentContainer public CompositeDrawable TabbableContentContainer
{ {
set => Component.TabbableContentContainer = value; set => Component.TabbableContentContainer = value;
} }

View File

@ -46,9 +46,6 @@ namespace osu.Game.Online.Solo
[JsonProperty("mods")] [JsonProperty("mods")]
public APIMod[] Mods { get; set; } public APIMod[] Mods { get; set; }
[JsonProperty("user")]
public APIUser User { get; set; }
[JsonProperty("statistics")] [JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; } public Dictionary<HitResult, int> Statistics { get; set; }
@ -67,7 +64,6 @@ namespace osu.Game.Online.Solo
RulesetID = score.RulesetID; RulesetID = score.RulesetID;
Passed = score.Passed; Passed = score.Passed;
Mods = score.APIMods; Mods = score.APIMods;
User = score.User;
Statistics = score.Statistics; Statistics = score.Statistics;
} }
} }

View File

@ -5,6 +5,7 @@ using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Runtime.Serialization;
using osu.Framework.Utils; using osu.Framework.Utils;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates that the object has not been judged yet. /// Indicates that the object has not been judged yet.
/// </summary> /// </summary>
[Description(@"")] [Description(@"")]
[EnumMember(Value = "none")]
[Order(14)] [Order(14)]
None, None,
@ -27,32 +29,39 @@ namespace osu.Game.Rulesets.Scoring
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
/// </remarks> /// </remarks>
[Description(@"Miss")] [Description(@"Miss")]
[EnumMember(Value = "miss")]
[Order(5)] [Order(5)]
Miss, Miss,
[Description(@"Meh")] [Description(@"Meh")]
[EnumMember(Value = "meh")]
[Order(4)] [Order(4)]
Meh, Meh,
[Description(@"OK")] [Description(@"OK")]
[EnumMember(Value = "ok")]
[Order(3)] [Order(3)]
Ok, Ok,
[Description(@"Good")] [Description(@"Good")]
[EnumMember(Value = "good")]
[Order(2)] [Order(2)]
Good, Good,
[Description(@"Great")] [Description(@"Great")]
[EnumMember(Value = "great")]
[Order(1)] [Order(1)]
Great, Great,
[Description(@"Perfect")] [Description(@"Perfect")]
[EnumMember(Value = "perfect")]
[Order(0)] [Order(0)]
Perfect, Perfect,
/// <summary> /// <summary>
/// Indicates small tick miss. /// Indicates small tick miss.
/// </summary> /// </summary>
[EnumMember(Value = "small_tick_miss")]
[Order(11)] [Order(11)]
SmallTickMiss, SmallTickMiss,
@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a small tick hit. /// Indicates a small tick hit.
/// </summary> /// </summary>
[Description(@"S Tick")] [Description(@"S Tick")]
[EnumMember(Value = "small_tick_hit")]
[Order(7)] [Order(7)]
SmallTickHit, SmallTickHit,
/// <summary> /// <summary>
/// Indicates a large tick miss. /// Indicates a large tick miss.
/// </summary> /// </summary>
[EnumMember(Value = "large_tick_miss")]
[Order(10)] [Order(10)]
LargeTickMiss, LargeTickMiss,
@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large tick hit. /// Indicates a large tick hit.
/// </summary> /// </summary>
[Description(@"L Tick")] [Description(@"L Tick")]
[EnumMember(Value = "large_tick_hit")]
[Order(6)] [Order(6)]
LargeTickHit, LargeTickHit,
@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a small bonus. /// Indicates a small bonus.
/// </summary> /// </summary>
[Description("S Bonus")] [Description("S Bonus")]
[EnumMember(Value = "small_bonus")]
[Order(9)] [Order(9)]
SmallBonus, SmallBonus,
@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates a large bonus. /// Indicates a large bonus.
/// </summary> /// </summary>
[Description("L Bonus")] [Description("L Bonus")]
[EnumMember(Value = "large_bonus")]
[Order(8)] [Order(8)]
LargeBonus, LargeBonus,
/// <summary> /// <summary>
/// Indicates a miss that should be ignored for scoring purposes. /// Indicates a miss that should be ignored for scoring purposes.
/// </summary> /// </summary>
[EnumMember(Value = "ignore_miss")]
[Order(13)] [Order(13)]
IgnoreMiss, IgnoreMiss,
/// <summary> /// <summary>
/// Indicates a hit that should be ignored for scoring purposes. /// Indicates a hit that should be ignored for scoring purposes.
/// </summary> /// </summary>
[EnumMember(Value = "ignore_hit")]
[Order(12)] [Order(12)]
IgnoreHit, IgnoreHit,
} }
@ -133,6 +149,30 @@ namespace osu.Game.Rulesets.Scoring
public static bool AffectsAccuracy(this HitResult result) public static bool AffectsAccuracy(this HitResult result)
=> IsScorable(result) && !IsBonus(result); => IsScorable(result) && !IsBonus(result);
/// <summary>
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
/// </summary>
public static bool IsBasic(this HitResult result)
=> IsScorable(result) && !IsTick(result) && !IsBonus(result);
/// <summary>
/// Whether a <see cref="HitResult"/> should be counted as a tick.
/// </summary>
public static bool IsTick(this HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
case HitResult.LargeTickMiss:
case HitResult.SmallTickHit:
case HitResult.SmallTickMiss:
return true;
default:
return false;
}
}
/// <summary> /// <summary>
/// Whether a <see cref="HitResult"/> should be counted as bonus score. /// Whether a <see cref="HitResult"/> should be counted as bonus score.
/// </summary> /// </summary>

View File

@ -75,9 +75,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
FillFlowContainer flow;
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer flow = new FillFlowContainer
{ {
Width = 200, Width = 200,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
@ -94,6 +96,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
}; };
bank.TabbableContentContainer = flow;
volume.TabbableContentContainer = flow;
// if the piece belongs to a currently selected object, assume that the user wants to change all selected objects. // if the piece belongs to a currently selected object, assume that the user wants to change all selected objects.
// if the piece belongs to an unselected object, operate on that object alone, independently of the selection. // if the piece belongs to an unselected object, operate on that object alone, independently of the selection.
var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray(); var relevantObjects = (beatmap.SelectedHitObjects.Contains(hitObject) ? beatmap.SelectedHitObjects : hitObject.Yield()).ToArray();

View File

@ -32,6 +32,11 @@ namespace osu.Game.Screens.Edit.Timing
set => slider.KeyboardStep = value; set => slider.KeyboardStep = value;
} }
public CompositeDrawable TabbableContentContainer
{
set => textBox.TabbableContentContainer = value;
}
private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>(); private readonly BindableWithCurrent<T?> current = new BindableWithCurrent<T?>();
public Bindable<T?> Current public Bindable<T?> Current

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
@ -18,6 +20,8 @@ namespace osu.Game.Screens.Play
public Container OverlayLayerContainer { get; private set; } public Container OverlayLayerContainer { get; private set; }
private readonly Storyboard storyboard; private readonly Storyboard storyboard;
private readonly IReadOnlyList<Mod> mods;
private DrawableStoryboard drawableStoryboard; private DrawableStoryboard drawableStoryboard;
/// <summary> /// <summary>
@ -28,9 +32,10 @@ namespace osu.Game.Screens.Play
/// </remarks> /// </remarks>
public IBindable<bool> HasStoryboardEnded = new BindableBool(true); public IBindable<bool> HasStoryboardEnded = new BindableBool(true);
public DimmableStoryboard(Storyboard storyboard) public DimmableStoryboard(Storyboard storyboard, IReadOnlyList<Mod> mods)
{ {
this.storyboard = storyboard; this.storyboard = storyboard;
this.mods = mods;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -57,7 +62,7 @@ namespace osu.Game.Screens.Play
if (!ShowStoryboard.Value && !IgnoreUserSettings.Value) if (!ShowStoryboard.Value && !IgnoreUserSettings.Value)
return; return;
drawableStoryboard = storyboard.CreateDrawable(); drawableStoryboard = storyboard.CreateDrawable(mods);
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded); HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
if (async) if (async)

View File

@ -3,7 +3,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Linq; using System.Linq;
using osu.Framework; using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -86,26 +85,10 @@ namespace osu.Game.Screens.Play
userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset); userAudioOffset = config.GetBindable<double>(OsuSetting.AudioOffset);
userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true); userAudioOffset.BindValueChanged(offset => userGlobalOffsetClock.Offset = offset.NewValue, true);
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
{ r => r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings,
var userSettings = r.Find<BeatmapInfo>(beatmap.BeatmapInfo.ID)?.UserSettings; settings => settings.Offset,
val => userBeatmapOffsetClock.Offset = val);
if (userSettings == null) // only the case for tests.
return null;
void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
updateOffset();
}
updateOffset();
userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
void updateOffset() => userBeatmapOffsetClock.Offset = userSettings.Offset;
});
// sane default provided by ruleset. // sane default provided by ruleset.
startOffset = gameplayStartTime; startOffset = gameplayStartTime;

View File

@ -359,7 +359,7 @@ namespace osu.Game.Screens.Play
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart); protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents() => private Drawable createUnderlayComponents() =>
DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard, GameplayState.Mods) { RelativeSizeAxes = Axes.Both };
private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay) private Drawable createGameplayComponents(IWorkingBeatmap working) => new ScalingContainer(ScalingMode.Gameplay)
{ {

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.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -120,24 +119,10 @@ namespace osu.Game.Screens.Play.PlayerSettings
ReferenceScore.BindValueChanged(scoreChanged, true); ReferenceScore.BindValueChanged(scoreChanged, true);
beatmapOffsetSubscription = realm.RegisterCustomSubscription(r => beatmapOffsetSubscription = realm.SubscribeToPropertyChanged(
{ r => r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings,
var userSettings = r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID)?.UserSettings; settings => settings.Offset,
val => Current.Value = val);
if (userSettings == null) // only the case for tests.
return null;
Current.Value = userSettings.Offset;
userSettings.PropertyChanged += onUserSettingsOnPropertyChanged;
return new InvokeOnDisposal(() => userSettings.PropertyChanged -= onUserSettingsOnPropertyChanged);
void onUserSettingsOnPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(BeatmapUserSettings.Offset))
Current.Value = userSettings.Offset;
}
});
Current.BindValueChanged(currentChanged); Current.BindValueChanged(currentChanged);
} }

View File

@ -16,6 +16,9 @@ namespace osu.Game.Screens.Select.Carousel
{ {
public class SetPanelContent : CompositeDrawable public class SetPanelContent : CompositeDrawable
{ {
// Disallow interacting with difficulty icons on a panel until the panel has been selected.
public override bool PropagatePositionalInputSubTree => carouselSet.State.Value == CarouselItemState.Selected;
private readonly CarouselBeatmapSet carouselSet; private readonly CarouselBeatmapSet carouselSet;
public SetPanelContent(CarouselBeatmapSet carouselSet) public SetPanelContent(CarouselBeatmapSet carouselSet)

View File

@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
return; return;
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.
if (changes?.HasCollectionChanges() == false)
return;
var scores = sender.AsEnumerable(); var scores = sender.AsEnumerable();
if (filterMods && !mods.Value.Any()) if (filterMods && !mods.Value.Any())

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using osuTK; using osuTK;
@ -11,6 +13,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Stores; using osu.Game.Stores;
@ -50,14 +53,18 @@ namespace osu.Game.Storyboards.Drawables
private double? lastEventEndTime; private double? lastEventEndTime;
[Cached(typeof(IReadOnlyList<Mod>))]
public IReadOnlyList<Mod> Mods { get; }
private DependencyContainer dependencies; private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
public DrawableStoryboard(Storyboard storyboard) public DrawableStoryboard(Storyboard storyboard, IReadOnlyList<Mod> mods = null)
{ {
Storyboard = storyboard; Storyboard = storyboard;
Mods = mods ?? Array.Empty<Mod>();
Size = new Vector2(640, 480); Size = new Vector2(640, 480);

View File

@ -28,19 +28,22 @@ namespace osu.Game.Storyboards.Drawables
LifetimeStart = sampleInfo.StartTime; LifetimeStart = sampleInfo.StartTime;
} }
[Resolved] [Resolved(CanBeNull = true)]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } private IReadOnlyList<Mod> mods { get; set; }
protected override void SkinChanged(ISkinSource skin) protected override void SkinChanged(ISkinSource skin)
{ {
base.SkinChanged(skin); base.SkinChanged(skin);
foreach (var mod in mods.Value.OfType<IApplicableToSample>()) if (mods != null)
{
foreach (var mod in mods.OfType<IApplicableToSample>())
{ {
foreach (var sample in DrawableSamples) foreach (var sample in DrawableSamples)
mod.ApplyToSample(sample); mod.ApplyToSample(sample);
} }
} }
}
protected override void SamplePlaybackDisabledChanged(ValueChangedEvent<bool> disabled) protected override void SamplePlaybackDisabledChanged(ValueChangedEvent<bool> disabled)
{ {

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Rulesets.Mods;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards.Drawables; using osu.Game.Storyboards.Drawables;
@ -90,8 +91,8 @@ namespace osu.Game.Storyboards
} }
} }
public DrawableStoryboard CreateDrawable(IWorkingBeatmap working = null) => public DrawableStoryboard CreateDrawable(IReadOnlyList<Mod> mods = null) =>
new DrawableStoryboard(this); new DrawableStoryboard(this, mods);
public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore)
{ {

View File

@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset; public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
/// <summary>
/// Mods from *player* (not OsuScreen).
/// </summary>
public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods; public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods;
public new HUDOverlay HUDOverlay => base.HUDOverlay; public new HUDOverlay HUDOverlay => base.HUDOverlay;