1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 21:02:54 +08:00

Merge branch 'master' into tourney-switching-ui

This commit is contained in:
Shivam 2021-01-10 17:34:03 +01:00
commit 959696c296
67 changed files with 798 additions and 319 deletions

View File

@ -18,7 +18,7 @@
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.1" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="3.3.2" PrivateAssets="All" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Code Analysis"> <PropertyGroup Label="Code Analysis">
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet>
@ -28,9 +28,17 @@
<NoWarn>$(NoWarn);CS1591</NoWarn> <NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Project"> <PropertyGroup Label="Project">
<!-- DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway. <!--
This is required due to https://github.com/NuGet/Home/issues/5740 --> NU1701:
<NoWarn>$(NoWarn);NU1701</NoWarn> DeepEqual is not netstandard-compatible. This is fine since we run tests with .NET Framework anyway.
This is required due to https://github.com/NuGet/Home/issues/5740
CA9998:
Microsoft.CodeAnalysis.FxCopAnalyzers has been deprecated.
The entire package will be able to be removed after migrating to .NET 5,
as analysers are shipped as part of the .NET 5 SDK anyway.
-->
<NoWarn>$(NoWarn);NU1701;CA9998</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Nuget"> <PropertyGroup Label="Nuget">
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
@ -40,7 +48,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl> <RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes> <PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<Company>ppy Pty Ltd</Company> <Company>ppy Pty Ltd</Company>
<Copyright>Copyright (c) 2020 ppy Pty Ltd</Copyright> <Copyright>Copyright (c) 2021 ppy Pty Ltd</Copyright>
<PackageTags>osu game</PackageTags> <PackageTags>osu game</PackageTags>
</PropertyGroup> </PropertyGroup>
</Project> </Project>

View File

@ -1,4 +1,4 @@
Copyright (c) 2020 ppy Pty Ltd <contact@ppy.sh>. Copyright (c) 2021 ppy Pty Ltd <contact@ppy.sh>.
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1229.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.106.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -11,7 +11,7 @@
<requireLicenseAcceptance>false</requireLicenseAcceptance> <requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>A free-to-win rhythm game. Rhythm is just a *click* away!</description> <description>A free-to-win rhythm game. Rhythm is just a *click* away!</description>
<releaseNotes>testing</releaseNotes> <releaseNotes>testing</releaseNotes>
<copyright>Copyright (c) 2020 ppy Pty Ltd</copyright> <copyright>Copyright (c) 2021 ppy Pty Ltd</copyright>
<language>en-AU</language> <language>en-AU</language>
</metadata> </metadata>
<files> <files>

View File

@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{ {
base.ApplySettings(difficulty); base.ApplySettings(difficulty);
difficulty.CircleSize = CircleSize.Value; ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
difficulty.ApproachRate = ApproachRate.Value; ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
} }
} }
} }

View File

@ -1,13 +1,17 @@
// 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 System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
@ -18,8 +22,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
public void TestNoAdjustment() => CreateModTest(new ModTestData public void TestNoAdjustment() => CreateModTest(new ModTestData
{ {
Mod = new OsuModDifficultyAdjust(), Mod = new OsuModDifficultyAdjust(),
Beatmap = new Beatmap
{
BeatmapInfo = new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 8
}
},
HitObjects = new List<HitObject>
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 2000 }
}
},
Autoplay = true, Autoplay = true,
PassCondition = checkSomeHit PassCondition = () => checkSomeHit() && checkObjectsScale(0.29f)
}); });
[Test] [Test]

View File

@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
base.ApplySettings(difficulty); base.ApplySettings(difficulty);
difficulty.CircleSize = CircleSize.Value; ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
difficulty.ApproachRate = ApproachRate.Value; ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
} }
} }
} }

View File

@ -246,5 +246,32 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0));
Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0));
} }
[Test]
public void TestCreateCopyIsDeepClone()
{
var cpi = new ControlPointInfo();
cpi.Add(1000, new TimingControlPoint { BeatLength = 500 });
var cpiCopy = cpi.CreateCopy();
cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 });
Assert.That(cpi.Groups.Count, Is.EqualTo(1));
Assert.That(cpiCopy.Groups.Count, Is.EqualTo(2));
Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1));
Assert.That(cpiCopy.TimingPoints.Count, Is.EqualTo(2));
Assert.That(cpi.TimingPoints[0], Is.Not.SameAs(cpiCopy.TimingPoints[0]));
Assert.That(cpi.TimingPoints[0].BeatLengthBindable, Is.Not.SameAs(cpiCopy.TimingPoints[0].BeatLengthBindable));
Assert.That(cpi.TimingPoints[0].BeatLength, Is.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
cpi.TimingPoints[0].BeatLength = 800;
Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength));
}
} }
} }

View File

@ -0,0 +1,50 @@
// 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;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Screens.OnlinePlay.Multiplayer;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene
{
private CreateMultiplayerMatchButton button;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create button", () => Child = button = new CreateMultiplayerMatchButton
{
Width = 200,
Height = 100,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestButtonEnableStateChanges()
{
IDisposable joiningRoomOperation = null;
assertButtonEnableState(true);
AddStep("begin joining room", () => joiningRoomOperation = OngoingOperationTracker.BeginOperation());
assertButtonEnableState(false);
AddStep("end joining room", () => joiningRoomOperation.Dispose());
assertButtonEnableState(true);
AddStep("disconnect client", () => Client.Disconnect());
assertButtonEnableState(false);
AddStep("re-connect client", () => Client.Connect());
assertButtonEnableState(true);
}
private void assertButtonEnableState(bool enabled)
=> AddAssert($"button {(enabled ? "enabled" : "disabled")}", () => button.Enabled.Value == enabled);
}
}

View File

@ -3,10 +3,12 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -18,6 +20,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
private MultiplayerMatchSubScreen screen; private MultiplayerMatchSubScreen screen;
[Cached]
private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
public TestSceneMultiplayerMatchSubScreen() public TestSceneMultiplayerMatchSubScreen()
: base(false) : base(false)
{ {

View File

@ -1,6 +1,7 @@
// 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.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -30,6 +31,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
private RulesetStore rulesets; private RulesetStore rulesets;
private IDisposable readyClickOperation;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
@ -56,6 +59,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
Beatmap = { Value = Beatmap.Value.BeatmapInfo }, Beatmap = { Value = Beatmap.Value.BeatmapInfo },
Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }
} }
},
OnReadyClick = async () =>
{
readyClickOperation = OngoingOperationTracker.BeginOperation();
if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready)
{
await Client.StartMatch();
return;
}
await Client.ToggleReady();
readyClickOperation.Dispose();
} }
}; };
}); });
@ -108,8 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addClickButtonStep(); addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep(); verifyGameplayStartFlow();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
} }
[Test] [Test]
@ -124,8 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addClickButtonStep(); addClickButtonStep();
AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0));
addClickButtonStep(); verifyGameplayStartFlow();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
} }
[Test] [Test]
@ -179,5 +193,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(button); InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
private void verifyGameplayStartFlow()
{
addClickButtonStep();
AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
} }
} }

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Details;
using osuTK.Graphics; using osuTK.Graphics;
@ -141,16 +142,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select changed Difficulty Adjust mod", () => AddStep("select changed Difficulty Adjust mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<ModDifficultyAdjust>().Single(); var difficultyAdjustMod = ruleset.GetAllMods().OfType<OsuModDifficultyAdjust>().Single();
var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; var originalDifficulty = advancedStats.Beatmap.BaseDifficulty;
var adjustedDifficulty = new BeatmapDifficulty
{ difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
CircleSize = originalDifficulty.CircleSize, difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
DrainRate = originalDifficulty.DrainRate - 0.5f, difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
OverallDifficulty = originalDifficulty.OverallDifficulty,
ApproachRate = originalDifficulty.ApproachRate + 2.2f,
};
difficultyAdjustMod.ReadFromDifficulty(adjustedDifficulty);
SelectedMods.Value = new[] { difficultyAdjustMod }; SelectedMods.Value = new[] { difficultyAdjustMod };
}); });

View File

@ -131,6 +131,18 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0); AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0);
} }
[Test]
public void TestExternallySetCustomizedMod()
{
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("ensure button is selected and customized accordingly", () =>
{
var button = modSelect.GetModButton(SelectedMods.Value.Single());
return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01;
});
}
private void testSingleMod(Mod mod) private void testSingleMod(Mod mod)
{ {
selectNext(mod); selectNext(mod);

View File

@ -0,0 +1,65 @@
// 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;
using System.Diagnostics;
using System.Drawing;
using Newtonsoft.Json;
namespace osu.Game.Tournament
{
/// <summary>
/// We made a change from using SixLabors.ImageSharp.Point to System.Drawing.Point at some stage.
/// This handles converting to a standardised format on json serialize/deserialize operations.
/// </summary>
internal class JsonPointConverter : JsonConverter<Point>
{
public override void WriteJson(JsonWriter writer, Point value, JsonSerializer serializer)
{
// use the format of LaborSharp's Point since it is nicer.
serializer.Serialize(writer, new { value.X, value.Y });
}
public override Point ReadJson(JsonReader reader, Type objectType, Point existingValue, bool hasExistingValue, JsonSerializer serializer)
{
if (reader.TokenType != JsonToken.StartObject)
{
// if there's no object present then this is using string representation (System.Drawing.Point serializes to "x,y")
string str = (string)reader.Value;
Debug.Assert(str != null);
return new PointConverter().ConvertFromString(str) as Point? ?? new Point();
}
var point = new Point();
while (reader.Read())
{
if (reader.TokenType == JsonToken.EndObject) break;
if (reader.TokenType == JsonToken.PropertyName)
{
var name = reader.Value?.ToString();
int? val = reader.ReadAsInt32();
if (val == null)
continue;
switch (name)
{
case "X":
point.X = val.Value;
break;
case "Y":
point.Y = val.Value;
break;
}
}
}
return point;
}
}
}

View File

@ -8,12 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.IO; using osu.Game.Tournament.IO;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Input; using osuTK.Input;
@ -60,7 +60,7 @@ namespace osu.Game.Tournament
{ {
using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open)) using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
using (var sr = new StreamReader(stream)) using (var sr = new StreamReader(stream))
ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd()); ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd(), new JsonPointConverter());
} }
ladder ??= new LadderInfo(); ladder ??= new LadderInfo();
@ -251,6 +251,7 @@ namespace osu.Game.Tournament
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore, NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
})); }));
} }
} }

View File

@ -50,7 +50,15 @@ namespace osu.Game.Beatmaps
IBeatmap IBeatmap.Clone() => Clone(); IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone(); public Beatmap<T> Clone()
{
var clone = (Beatmap<T>)MemberwiseClone();
clone.ControlPointInfo = ControlPointInfo.CreateCopy();
// todo: deep clone other elements as required.
return clone;
}
} }
public class Beatmap : Beatmap<HitObject> public class Beatmap : Beatmap<HitObject>

View File

@ -28,5 +28,21 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <param name="existing">An existing control point to compare with.</param> /// <param name="existing">An existing control point to compare with.</param>
/// <returns>Whether this <see cref="ControlPoint"/> is redundant when placed alongside <paramref name="existing"/>.</returns> /// <returns>Whether this <see cref="ControlPoint"/> is redundant when placed alongside <paramref name="existing"/>.</returns>
public abstract bool IsRedundant(ControlPoint existing); public abstract bool IsRedundant(ControlPoint existing);
/// <summary>
/// Create an unbound copy of this control point.
/// </summary>
public ControlPoint CreateCopy()
{
var copy = (ControlPoint)Activator.CreateInstance(GetType());
copy.CopyFrom(this);
return copy;
}
public virtual void CopyFrom(ControlPoint other)
{
}
} }
} }

View File

@ -297,5 +297,15 @@ namespace osu.Game.Beatmaps.ControlPoints
break; break;
} }
} }
public ControlPointInfo CreateCopy()
{
var controlPointInfo = new ControlPointInfo();
foreach (var point in AllControlPoints)
controlPointInfo.Add(point.Time, point.CreateCopy());
return controlPointInfo;
}
} }
} }

View File

@ -39,5 +39,12 @@ namespace osu.Game.Beatmaps.ControlPoints
public override bool IsRedundant(ControlPoint existing) public override bool IsRedundant(ControlPoint existing)
=> existing is DifficultyControlPoint existingDifficulty => existing is DifficultyControlPoint existingDifficulty
&& SpeedMultiplier == existingDifficulty.SpeedMultiplier; && SpeedMultiplier == existingDifficulty.SpeedMultiplier;
public override void CopyFrom(ControlPoint other)
{
SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier;
base.CopyFrom(other);
}
} }
} }

View File

@ -50,5 +50,13 @@ namespace osu.Game.Beatmaps.ControlPoints
&& existing is EffectControlPoint existingEffect && existing is EffectControlPoint existingEffect
&& KiaiMode == existingEffect.KiaiMode && KiaiMode == existingEffect.KiaiMode
&& OmitFirstBarLine == existingEffect.OmitFirstBarLine; && OmitFirstBarLine == existingEffect.OmitFirstBarLine;
public override void CopyFrom(ControlPoint other)
{
KiaiMode = ((EffectControlPoint)other).KiaiMode;
OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine;
base.CopyFrom(other);
}
} }
} }

View File

@ -72,5 +72,13 @@ namespace osu.Game.Beatmaps.ControlPoints
=> existing is SampleControlPoint existingSample => existing is SampleControlPoint existingSample
&& SampleBank == existingSample.SampleBank && SampleBank == existingSample.SampleBank
&& SampleVolume == existingSample.SampleVolume; && SampleVolume == existingSample.SampleVolume;
public override void CopyFrom(ControlPoint other)
{
SampleVolume = ((SampleControlPoint)other).SampleVolume;
SampleBank = ((SampleControlPoint)other).SampleBank;
base.CopyFrom(other);
}
} }
} }

View File

@ -69,5 +69,13 @@ namespace osu.Game.Beatmaps.ControlPoints
// Timing points are never redundant as they can change the time signature. // Timing points are never redundant as they can change the time signature.
public override bool IsRedundant(ControlPoint existing) => false; public override bool IsRedundant(ControlPoint existing) => false;
public override void CopyFrom(ControlPoint other)
{
TimeSignature = ((TimingControlPoint)other).TimeSignature;
BeatLength = ((TimingControlPoint)other).BeatLength;
base.CopyFrom(other);
}
} }
} }

View File

@ -164,13 +164,24 @@ namespace osu.Game.Beatmaps.Formats
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE. /// DO NOT USE THIS UNLESS 100% SURE.
/// </summary> /// </summary>
public readonly float BpmMultiplier; public float BpmMultiplier { get; private set; }
public LegacyDifficultyControlPoint(double beatLength) public LegacyDifficultyControlPoint(double beatLength)
: this()
{
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1;
}
public LegacyDifficultyControlPoint()
{ {
SpeedMultiplierBindable.Precision = double.Epsilon; SpeedMultiplierBindable.Precision = double.Epsilon;
}
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1; public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);
BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier;
} }
} }
@ -192,6 +203,13 @@ namespace osu.Game.Beatmaps.Formats
=> base.IsRedundant(existing) => base.IsRedundant(existing)
&& existing is LegacySampleControlPoint existingSample && existing is LegacySampleControlPoint existingSample
&& CustomSampleBank == existingSample.CustomSampleBank; && CustomSampleBank == existingSample.CustomSampleBank;
public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);
CustomSampleBank = ((LegacySampleControlPoint)other).CustomSampleBank;
}
} }
} }
} }

View File

@ -1,7 +1,9 @@
// 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 osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
@ -14,6 +16,7 @@ namespace osu.Game.Configuration
{ {
Set(Static.LoginOverlayDisplayed, false); Set(Static.LoginOverlayDisplayed, false);
Set(Static.MutedAudioNotificationShownOnce, false); Set(Static.MutedAudioNotificationShownOnce, false);
Set(Static.LastHoverSoundPlaybackTime, (double?)null);
Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
} }
} }
@ -28,5 +31,11 @@ namespace osu.Game.Configuration
/// Value under this lookup can be <c>null</c> if there are no backgrounds available (or API is not reachable). /// Value under this lookup can be <c>null</c> if there are no backgrounds available (or API is not reachable).
/// </summary> /// </summary>
SeasonalBackgrounds, SeasonalBackgrounds,
/// <summary>
/// The last playback time in milliseconds of a hover sample (from <see cref="HoverSounds"/>).
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
/// </summary>
LastHoverSoundPlaybackTime
} }
} }

View File

@ -5,11 +5,12 @@ using System.ComponentModel;
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.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Game.Configuration;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -22,36 +23,39 @@ namespace osu.Game.Graphics.UserInterface
private SampleChannel sampleHover; private SampleChannel sampleHover;
/// <summary> /// <summary>
/// Length of debounce for hover sound playback, in milliseconds. Default is 50ms. /// Length of debounce for hover sound playback, in milliseconds.
/// </summary> /// </summary>
public double HoverDebounceTime { get; } = 50; public double HoverDebounceTime { get; } = 20;
protected readonly HoverSampleSet SampleSet; protected readonly HoverSampleSet SampleSet;
private Bindable<double?> lastPlaybackTime;
public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal)
{ {
SampleSet = sampleSet; SampleSet = sampleSet;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
private ScheduledDelegate playDelegate; [BackgroundDependencyLoader]
private void load(AudioManager audio, SessionStatics statics)
{
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
}
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
playDelegate?.Cancel(); bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
if (HoverDebounceTime <= 0) if (enoughTimePassedSinceLastPlayback)
{
sampleHover?.Play(); sampleHover?.Play();
else lastPlaybackTime.Value = Time.Current;
playDelegate = Scheduler.AddDelayed(() => sampleHover?.Play(), HoverDebounceTime);
return base.OnHover(e);
} }
[BackgroundDependencyLoader] return base.OnHover(e);
private void load(AudioManager audio)
{
sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}");
} }
} }

View File

@ -65,6 +65,23 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>(); public readonly BindableList<int> CurrentMatchPlayingUserIds = new BindableList<int>();
/// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary>
public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id);
/// <summary>
/// Whether the <see cref="LocalUser"/> is the host in <see cref="Room"/>.
/// </summary>
public bool IsHost
{
get
{
var localUser = LocalUser;
return localUser != null && Room?.Host != null && localUser.Equals(Room.Host);
}
}
[Resolved] [Resolved]
private UserLookupCache userLookupCache { get; set; } = null!; private UserLookupCache userLookupCache { get; set; } = null!;
@ -178,6 +195,32 @@ namespace osu.Game.Online.Multiplayer
}); });
} }
/// <summary>
/// Toggles the <see cref="LocalUser"/>'s ready state.
/// </summary>
/// <exception cref="InvalidOperationException">If a toggle of ready state is not valid at this time.</exception>
public async Task ToggleReady()
{
var localUser = LocalUser;
if (localUser == null)
return;
switch (localUser.State)
{
case MultiplayerUserState.Idle:
await ChangeState(MultiplayerUserState.Ready);
return;
case MultiplayerUserState.Ready:
await ChangeState(MultiplayerUserState.Idle);
return;
default:
throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}");
}
}
public abstract Task TransferHost(int userId); public abstract Task TransferHost(int userId);
public abstract Task ChangeSettings(MultiplayerRoomSettings settings); public abstract Task ChangeSettings(MultiplayerRoomSettings settings);

View File

@ -52,9 +52,10 @@ namespace osu.Game.Overlays.Mods
if (newIndex == selectedIndex) return false; if (newIndex == selectedIndex) return false;
int direction = newIndex < selectedIndex ? -1 : 1; int direction = newIndex < selectedIndex ? -1 : 1;
bool beforeSelected = Selected; bool beforeSelected = Selected;
Mod modBefore = SelectedMod ?? Mods[0]; Mod previousSelection = SelectedMod ?? Mods[0];
if (newIndex >= Mods.Length) if (newIndex >= Mods.Length)
newIndex = -1; newIndex = -1;
@ -65,22 +66,25 @@ namespace osu.Game.Overlays.Mods
return false; return false;
selectedIndex = newIndex; selectedIndex = newIndex;
Mod modAfter = SelectedMod ?? Mods[0];
Mod newSelection = SelectedMod ?? Mods[0];
Schedule(() =>
{
if (beforeSelected != Selected) if (beforeSelected != Selected)
{ {
iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic);
iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic);
} }
if (modBefore != modAfter) if (previousSelection != newSelection)
{ {
const float rotate_angle = 16; const float rotate_angle = 16;
foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing);
backgroundIcon.Mod = modAfter; backgroundIcon.Mod = newSelection;
using (BeginDelayedSequence(mod_switch_duration, true)) using (BeginDelayedSequence(mod_switch_duration, true))
{ {
@ -92,13 +96,15 @@ namespace osu.Game.Overlays.Mods
.RotateTo(rotate_angle * direction) .RotateTo(rotate_angle * direction)
.RotateTo(0f, mod_switch_duration, mod_switch_easing); .RotateTo(0f, mod_switch_duration, mod_switch_easing);
Schedule(() => displayMod(modAfter)); Schedule(() => displayMod(newSelection));
} }
} }
foregroundIcon.Selected.Value = Selected; foregroundIcon.Selected.Value = Selected;
});
SelectionChanged?.Invoke(SelectedMod); SelectionChanged?.Invoke(SelectedMod);
return true; return true;
} }

View File

@ -127,20 +127,30 @@ namespace osu.Game.Overlays.Mods
} }
/// <summary> /// <summary>
/// Select one or more mods in this section and deselects all other ones. /// Updates all buttons with the given list of selected mods.
/// </summary> /// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be selected.</param> /// <param name="newSelectedMods">The new list of selected mods to select.</param>
public void SelectTypes(IEnumerable<Type> modTypes) public void UpdateSelectedMods(IReadOnlyList<Mod> newSelectedMods)
{ {
foreach (var button in buttons) foreach (var button in buttons)
{ updateButtonMods(button, newSelectedMods);
int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType()));
if (i >= 0)
button.SelectAt(i);
else
button.Deselect();
} }
private void updateButtonMods(ModButton button, IReadOnlyList<Mod> newSelectedMods)
{
foreach (var mod in newSelectedMods)
{
var index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType());
if (index < 0)
continue;
var buttonMod = button.Mods[index];
buttonMod.CopyFrom(mod);
button.SelectAt(index);
return;
}
button.Deselect();
} }
protected ModSection() protected ModSection()

View File

@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Mods
{ {
Width = 180, Width = 180,
Text = "Deselect All", Text = "Deselect All",
Action = DeselectAll, Action = deselectAll,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
}, },
@ -318,7 +318,7 @@ namespace osu.Game.Overlays.Mods
sampleOff = audio.Samples.Get(@"UI/check-off"); sampleOff = audio.Samples.Get(@"UI/check-off");
} }
public void DeselectAll() private void deselectAll()
{ {
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.DeselectAll(); section.DeselectAll();
@ -331,7 +331,7 @@ namespace osu.Game.Overlays.Mods
/// </summary> /// </summary>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param> /// <param name="modTypes">The types of <see cref="Mod"/>s which should be deselected.</param>
/// <param name="immediate">Set to true to bypass animations and update selections immediately.</param> /// <param name="immediate">Set to true to bypass animations and update selections immediately.</param>
public void DeselectTypes(Type[] modTypes, bool immediate = false) private void deselectTypes(Type[] modTypes, bool immediate = false)
{ {
if (modTypes.Length == 0) return; if (modTypes.Length == 0) return;
@ -409,7 +409,7 @@ namespace osu.Game.Overlays.Mods
private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods) private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{ {
foreach (var section in ModSectionsContainer.Children) foreach (var section in ModSectionsContainer.Children)
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); section.UpdateSelectedMods(mods.NewValue);
updateMods(); updateMods();
} }
@ -438,7 +438,7 @@ namespace osu.Game.Overlays.Mods
{ {
if (State.Value == Visibility.Visible) sampleOn?.Play(); if (State.Value == Visibility.Visible) sampleOn?.Play();
DeselectTypes(selectedMod.IncompatibleMods, true); deselectTypes(selectedMod.IncompatibleMods, true);
if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
} }

View File

@ -128,20 +128,29 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
public virtual Mod CreateCopy() public virtual Mod CreateCopy()
{ {
var copy = (Mod)Activator.CreateInstance(GetType()); var result = (Mod)Activator.CreateInstance(GetType());
result.CopyFrom(this);
// Copy bindable values across return result;
foreach (var (_, prop) in this.GetSettingsSourceProperties())
{
var origBindable = (IBindable)prop.GetValue(this);
var copyBindable = (IBindable)prop.GetValue(copy);
// we only care about changes that have been made away from defaults.
if (!origBindable.IsDefault)
copy.CopyAdjustedSetting(copyBindable, origBindable);
} }
return copy; /// <summary>
/// Copies mod setting values from <paramref name="source"/> into this instance.
/// </summary>
/// <param name="source">The mod to copy properties from.</param>
public void CopyFrom(Mod source)
{
if (source.GetType() != GetType())
throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source));
foreach (var (_, prop) in this.GetSettingsSourceProperties())
{
var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source);
// we only care about changes that have been made away from defaults.
if (!sourceBindable.IsDefault)
CopyAdjustedSetting(targetBindable, sourceBindable);
}
} }
/// <summary> /// <summary>

View File

@ -116,18 +116,30 @@ namespace osu.Game.Rulesets.Mods
internal override void CopyAdjustedSetting(IBindable target, object source) internal override void CopyAdjustedSetting(IBindable target, object source)
{ {
userChangedSettings[target] = true; // if the value is non-bindable, it's presumably coming from an external source (like the API) - therefore presume it is not default.
// if the value is bindable, defer to the source's IsDefault to be able to tell.
userChangedSettings[target] = !(source is IBindable bindableSource) || !bindableSource.IsDefault;
base.CopyAdjustedSetting(target, source); base.CopyAdjustedSetting(target, source);
} }
/// <summary>
/// Applies a setting from a configuration bindable using <paramref name="applyFunc"/>, if it has been changed by the user.
/// </summary>
protected void ApplySetting<T>(BindableNumber<T> setting, Action<T> applyFunc)
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
{
if (userChangedSettings.TryGetValue(setting, out bool userChangedSetting) && userChangedSetting)
applyFunc.Invoke(setting.Value);
}
/// <summary> /// <summary>
/// Apply all custom settings to the provided beatmap. /// Apply all custom settings to the provided beatmap.
/// </summary> /// </summary>
/// <param name="difficulty">The beatmap to have settings applied.</param> /// <param name="difficulty">The beatmap to have settings applied.</param>
protected virtual void ApplySettings(BeatmapDifficulty difficulty) protected virtual void ApplySettings(BeatmapDifficulty difficulty)
{ {
difficulty.DrainRate = DrainRate.Value; ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
difficulty.OverallDifficulty = OverallDifficulty.Value; ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
} }
} }
} }

View File

@ -750,7 +750,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Result.Type != originalType) if (Result.Type != originalType)
{ {
Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n" Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n"
+ $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards.", level: LogLevel.Important); + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2021-03-28 onwards.", level: LogLevel.Important);
} }
} }

View File

@ -10,10 +10,7 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset
#pragma warning disable 618
IHasCurve
#pragma warning restore 618
{ {
/// <summary> /// <summary>
/// Scoring distance with a speed-adjusted beat length of 1 second. /// Scoring distance with a speed-adjusted beat length of 1 second.

View File

@ -1,55 +0,0 @@
// 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;
using osuTK;
namespace osu.Game.Rulesets.Objects.Types
{
[Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public interface IHasCurve : IHasDistance, IHasRepeats
{
/// <summary>
/// The curve.
/// </summary>
SliderPath Path { get; }
}
#pragma warning disable 618
[Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public static class HasCurveExtensions
{
/// <summary>
/// Computes the position on the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>The position on the curve.</returns>
public static Vector2 CurvePositionAt(this IHasCurve obj, double progress)
=> obj.Path.PositionAt(obj.ProgressAt(progress));
/// <summary>
/// Computes the progress along the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</returns>
public static double ProgressAt(this IHasCurve obj, double progress)
{
double p = progress * obj.SpanCount() % 1;
if (obj.SpanAt(progress) % 2 == 1)
p = 1 - p;
return p;
}
/// <summary>
/// Determines which span of the curve the progress point is on.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
/// <returns>[0, SpanCount) where 0 is the first run.</returns>
public static int SpanAt(this IHasCurve obj, double progress)
=> (int)(progress * obj.SpanCount());
}
#pragma warning restore 618
}

View File

@ -6,26 +6,16 @@ namespace osu.Game.Rulesets.Objects.Types
/// <summary> /// <summary>
/// A HitObject that ends at a different time than its start time. /// A HitObject that ends at a different time than its start time.
/// </summary> /// </summary>
#pragma warning disable 618 public interface IHasDuration
public interface IHasDuration : IHasEndTime
#pragma warning restore 618
{ {
double IHasEndTime.EndTime
{
get => EndTime;
set => Duration = (Duration - EndTime) + value;
}
double IHasEndTime.Duration => Duration;
/// <summary> /// <summary>
/// The time at which the HitObject ends. /// The time at which the HitObject ends.
/// </summary> /// </summary>
new double EndTime { get; } double EndTime { get; }
/// <summary> /// <summary>
/// The duration of the HitObject. /// The duration of the HitObject.
/// </summary> /// </summary>
new double Duration { get; set; } double Duration { get; set; }
} }
} }

View File

@ -1,26 +0,0 @@
// 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;
using Newtonsoft.Json;
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A HitObject that ends at a different time than its start time.
/// </summary>
[Obsolete("Use IHasDuration instead.")] // can be removed 20201126
public interface IHasEndTime
{
/// <summary>
/// The time at which the HitObject ends.
/// </summary>
[JsonIgnore]
double EndTime { get; set; }
/// <summary>
/// The duration of the HitObject.
/// </summary>
double Duration { get; }
}
}

View File

@ -100,9 +100,7 @@ namespace osu.Game.Rulesets
foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{ {
// todo: StartsWith can be changed to Equals on 2020-11-08 if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
// This is to give users enough time to have their database use new abbreviated info).
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
context.RulesetInfo.Add(r.RulesetInfo); context.RulesetInfo.Add(r.RulesetInfo);
} }

View File

@ -16,6 +16,9 @@ namespace osu.Game.Screens.Edit.Compose
{ {
public class ComposeScreen : EditorScreenWithTimeline public class ComposeScreen : EditorScreenWithTimeline
{ {
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
private HitObjectComposer composer; private HitObjectComposer composer;
public ComposeScreen() public ComposeScreen()
@ -59,7 +62,7 @@ namespace osu.Game.Screens.Edit.Compose
{ {
Debug.Assert(ruleset != null); Debug.Assert(ruleset != null);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.Skin);
// the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation
// full access to all skin sources. // full access to all skin sources.

View File

@ -486,6 +486,8 @@ namespace osu.Game.Screens.Edit
ApplyToBackground(b => b.FadeColour(Color4.White, 500)); ApplyToBackground(b => b.FadeColour(Color4.White, 500));
resetTrack(); resetTrack();
Beatmap.Value = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
return base.OnExiting(next); return base.OnExiting(next);
} }

View File

@ -2,10 +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 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.Game.Beatmaps;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
@ -14,9 +12,6 @@ namespace osu.Game.Screens.Edit
/// </summary> /// </summary>
public abstract class EditorScreen : Container public abstract class EditorScreen : Container
{ {
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved] [Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; } protected EditorBeatmap EditorBeatmap { get; private set; }

View File

@ -30,16 +30,16 @@ namespace osu.Game.Screens.Edit
{ {
} }
private Container mainContent;
private LoadingSpinner spinner;
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor) private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{ {
if (beatDivisor != null) if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor); this.beatDivisor.BindTo(beatDivisor);
Container mainContent;
LoadingSpinner spinner;
Children = new Drawable[] Children = new Drawable[]
{ {
mainContent = new Container mainContent = new Container
@ -99,6 +99,11 @@ namespace osu.Game.Screens.Edit
} }
}, },
}; };
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(CreateMainContent(), content => LoadComponentAsync(CreateMainContent(), content =>
{ {

View File

@ -13,9 +13,6 @@ namespace osu.Game.Screens.Edit.Setup
{ {
internal class DifficultySection : SetupSection internal class DifficultySection : SetupSection
{ {
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
private LabelledSliderBar<float> circleSizeSlider; private LabelledSliderBar<float> circleSizeSlider;
private LabelledSliderBar<float> healthDrainSlider; private LabelledSliderBar<float> healthDrainSlider;
private LabelledSliderBar<float> approachRateSlider; private LabelledSliderBar<float> approachRateSlider;
@ -34,7 +31,7 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Label = "Object Size", Label = "Object Size",
Description = "The size of all hit objects", Description = "The size of all hit objects",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
@ -46,7 +43,7 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Label = "Health Drain", Label = "Health Drain",
Description = "The rate of passive health drain throughout playable time", Description = "The rate of passive health drain throughout playable time",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate) Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
@ -58,7 +55,7 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Label = "Approach Rate", Label = "Approach Rate",
Description = "The speed at which objects are presented to the player", Description = "The speed at which objects are presented to the player",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate) Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
@ -70,7 +67,7 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Label = "Overall Difficulty", Label = "Overall Difficulty",
Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)",
Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty) Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
@ -88,12 +85,12 @@ namespace osu.Game.Screens.Edit.Setup
{ {
// for now, update these on commit rather than making BeatmapMetadata bindables. // for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
editorBeatmap.UpdateAllHitObjects(); Beatmap.UpdateAllHitObjects();
} }
} }
} }

View File

@ -29,25 +29,25 @@ namespace osu.Game.Screens.Edit.Setup
artistTextBox = new LabelledTextBox artistTextBox = new LabelledTextBox
{ {
Label = "Artist", Label = "Artist",
Current = { Value = Beatmap.Value.Metadata.Artist }, Current = { Value = Beatmap.Metadata.Artist },
TabbableContentContainer = this TabbableContentContainer = this
}, },
titleTextBox = new LabelledTextBox titleTextBox = new LabelledTextBox
{ {
Label = "Title", Label = "Title",
Current = { Value = Beatmap.Value.Metadata.Title }, Current = { Value = Beatmap.Metadata.Title },
TabbableContentContainer = this TabbableContentContainer = this
}, },
creatorTextBox = new LabelledTextBox creatorTextBox = new LabelledTextBox
{ {
Label = "Creator", Label = "Creator",
Current = { Value = Beatmap.Value.Metadata.AuthorString }, Current = { Value = Beatmap.Metadata.AuthorString },
TabbableContentContainer = this TabbableContentContainer = this
}, },
difficultyTextBox = new LabelledTextBox difficultyTextBox = new LabelledTextBox
{ {
Label = "Difficulty Name", Label = "Difficulty Name",
Current = { Value = Beatmap.Value.BeatmapInfo.Version }, Current = { Value = Beatmap.BeatmapInfo.Version },
TabbableContentContainer = this TabbableContentContainer = this
}, },
}; };
@ -62,10 +62,10 @@ namespace osu.Game.Screens.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables. // for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; Beatmap.Metadata.Artist = artistTextBox.Current.Value;
Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; Beatmap.Metadata.Title = titleTextBox.Current.Value;
Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value;
Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value;
} }
} }
} }

View File

@ -42,6 +42,9 @@ namespace osu.Game.Screens.Edit.Setup
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; }
[Resolved]
private IBindable<WorkingBeatmap> working { get; set; }
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private Editor editor { get; set; } private Editor editor { get; set; }
@ -70,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup
audioTrackTextBox = new FileChooserLabelledTextBox audioTrackTextBox = new FileChooserLabelledTextBox
{ {
Label = "Audio Track", Label = "Audio Track",
Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, Current = { Value = working.Value.Metadata.AudioFile ?? "Click to select a track" },
Target = audioTrackFileChooserContainer, Target = audioTrackFileChooserContainer,
TabbableContentContainer = this TabbableContentContainer = this
}, },
@ -115,11 +118,11 @@ namespace osu.Game.Screens.Edit.Setup
if (!info.Exists) if (!info.Exists)
return false; return false;
var set = Beatmap.Value.BeatmapSetInfo; var set = working.Value.BeatmapSetInfo;
// remove the previous background for now. // remove the previous background for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?) // in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile); var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.BackgroundFile);
using (var stream = info.OpenRead()) using (var stream = info.OpenRead())
{ {
@ -129,7 +132,7 @@ namespace osu.Game.Screens.Edit.Setup
beatmaps.AddFile(set, stream, info.Name); beatmaps.AddFile(set, stream, info.Name);
} }
Beatmap.Value.Metadata.BackgroundFile = info.Name; working.Value.Metadata.BackgroundFile = info.Name;
updateBackgroundSprite(); updateBackgroundSprite();
return true; return true;
@ -148,11 +151,11 @@ namespace osu.Game.Screens.Edit.Setup
if (!info.Exists) if (!info.Exists)
return false; return false;
var set = Beatmap.Value.BeatmapSetInfo; var set = working.Value.BeatmapSetInfo;
// remove the previous audio track for now. // remove the previous audio track for now.
// in the future we probably want to check if this is being used elsewhere (other difficulties?) // in the future we probably want to check if this is being used elsewhere (other difficulties?)
var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.AudioFile);
using (var stream = info.OpenRead()) using (var stream = info.OpenRead())
{ {
@ -162,7 +165,7 @@ namespace osu.Game.Screens.Edit.Setup
beatmaps.AddFile(set, stream, info.Name); beatmaps.AddFile(set, stream, info.Name);
} }
Beatmap.Value.Metadata.AudioFile = info.Name; working.Value.Metadata.AudioFile = info.Name;
music.ReloadCurrentTrack(); music.ReloadCurrentTrack();
@ -178,7 +181,7 @@ namespace osu.Game.Screens.Edit.Setup
private void updateBackgroundSprite() private void updateBackgroundSprite()
{ {
LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) LoadComponentAsync(new BeatmapBackgroundSprite(working.Value)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -2,10 +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 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.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK; using osuTK;
@ -19,7 +17,7 @@ namespace osu.Game.Screens.Edit.Setup
protected OsuColour Colours { get; private set; } protected OsuColour Colours { get; private set; }
[Resolved] [Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
protected override Container<Drawable> Content => flow; protected override Container<Drawable> Content => flow;

View File

@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override DifficultyControlPoint CreatePoint() protected override DifficultyControlPoint CreatePoint()
{ {
var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time); var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time);
return new DifficultyControlPoint return new DifficultyControlPoint
{ {

View File

@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override EffectControlPoint CreatePoint() protected override EffectControlPoint CreatePoint()
{ {
var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time); var reference = Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time);
return new EffectControlPoint return new EffectControlPoint
{ {

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
@ -24,7 +23,7 @@ namespace osu.Game.Screens.Edit.Timing
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; } protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }
[Resolved] [Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
[Resolved] [Resolved]
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
@ -107,13 +106,13 @@ namespace osu.Game.Screens.Edit.Timing
var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray();
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value);
foreach (var cp in currentGroupItems) foreach (var cp in currentGroupItems)
Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); Beatmap.ControlPointInfo.Add(time, cp);
// the control point might not necessarily exist yet, if currentGroupItems was empty. // the control point might not necessarily exist yet, if currentGroupItems was empty.
SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time, true); SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true);
changeHandler?.EndChange(); changeHandler?.EndChange();
} }

View File

@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override SampleControlPoint CreatePoint() protected override SampleControlPoint CreatePoint()
{ {
var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time); var reference = Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time);
return new SampleControlPoint return new SampleControlPoint
{ {

View File

@ -7,7 +7,6 @@ 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.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -27,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing
private const float header_height = 20; private const float header_height = 20;
[Resolved] [Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
[Resolved] [Resolved]
protected Bindable<ControlPointGroup> SelectedGroup { get; private set; } protected Bindable<ControlPointGroup> SelectedGroup { get; private set; }

View File

@ -7,7 +7,6 @@ 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.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -62,7 +61,7 @@ namespace osu.Game.Screens.Edit.Timing
private EditorClock clock { get; set; } private EditorClock clock { get; set; }
[Resolved] [Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; } protected EditorBeatmap Beatmap { get; private set; }
[Resolved] [Resolved]
private Bindable<ControlPointGroup> selectedGroup { get; set; } private Bindable<ControlPointGroup> selectedGroup { get; set; }
@ -124,7 +123,7 @@ namespace osu.Game.Screens.Edit.Timing
selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true);
controlPointGroups.BindTo(Beatmap.Value.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((sender, args) => controlPointGroups.BindCollectionChanged((sender, args) =>
{ {
table.ControlGroups = controlPointGroups; table.ControlGroups = controlPointGroups;
@ -137,14 +136,14 @@ namespace osu.Game.Screens.Edit.Timing
if (selectedGroup.Value == null) if (selectedGroup.Value == null)
return; return;
Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value); Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime); selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
} }
private void addNew() private void addNew()
{ {
selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
} }
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override TimingControlPoint CreatePoint() protected override TimingControlPoint CreatePoint()
{ {
var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time); var reference = Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time);
return new TimingControlPoint return new TimingControlPoint
{ {

View File

@ -1,7 +1,10 @@
// 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.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -26,6 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby();
private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>(); private readonly IBindable<bool> initialRoomsReceived = new Bindable<bool>();
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private FilterControl filter; private FilterControl filter;
private Container content; private Container content;
@ -37,7 +41,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[Resolved] [Resolved]
private MusicController music { get; set; } private MusicController music { get; set; }
private bool joiningRoom; [Resolved(CanBeNull = true)]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[CanBeNull]
private IDisposable joiningRoomOperation { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -98,7 +106,13 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
base.LoadComplete(); base.LoadComplete();
initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived);
initialRoomsReceived.BindValueChanged(onInitialRoomsReceivedChanged, true); initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer());
if (ongoingOperationTracker != null)
{
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true);
}
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
@ -156,26 +170,24 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
private void joinRequested(Room room) private void joinRequested(Room room)
{ {
joiningRoom = true; Debug.Assert(joiningRoomOperation == null);
updateLoadingLayer(); joiningRoomOperation = ongoingOperationTracker?.BeginOperation();
RoomManager?.JoinRoom(room, r => RoomManager?.JoinRoom(room, r =>
{ {
Open(room); Open(room);
joiningRoom = false; joiningRoomOperation?.Dispose();
updateLoadingLayer(); joiningRoomOperation = null;
}, _ => }, _ =>
{ {
joiningRoom = false; joiningRoomOperation?.Dispose();
updateLoadingLayer(); joiningRoomOperation = null;
}); });
} }
private void onInitialRoomsReceivedChanged(ValueChangedEvent<bool> received) => updateLoadingLayer();
private void updateLoadingLayer() private void updateLoadingLayer()
{ {
if (joiningRoom || !initialRoomsReceived.Value) if (operationInProgress.Value || !initialRoomsReceived.Value)
loadingLayer.Show(); loadingLayer.Show();
else else
loadingLayer.Hide(); loadingLayer.Hide();

View File

@ -10,14 +10,34 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
public class CreateMultiplayerMatchButton : PurpleTriangleButton public class CreateMultiplayerMatchButton : PurpleTriangleButton
{ {
private IBindable<bool> isConnected;
private IBindable<bool> operationInProgress;
[Resolved]
private StatefulMultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(StatefulMultiplayerClient multiplayerClient) private void load()
{ {
Triangles.TriangleScale = 1.5f; Triangles.TriangleScale = 1.5f;
Text = "Create room"; Text = "Create room";
((IBindable<bool>)Enabled).BindTo(multiplayerClient.IsConnected); isConnected = multiplayerClient.IsConnected.GetBoundCopy();
} operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
isConnected.BindValueChanged(_ => updateState());
operationInProgress.BindValueChanged(_ => updateState(), true);
}
private void updateState() => Enabled.Value = isConnected.Value && !operationInProgress.Value;
} }
} }

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -19,7 +20,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>(); public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public Action OnReadyClick
{
set => readyButton.OnReadyClick = value;
}
private readonly Drawable background; private readonly Drawable background;
private readonly MultiplayerReadyButton readyButton;
public MultiplayerMatchFooter() public MultiplayerMatchFooter()
{ {
@ -29,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
InternalChildren = new[] InternalChildren = new[]
{ {
background = new Box { RelativeSizeAxes = Axes.Both }, background = new Box { RelativeSizeAxes = Axes.Both },
new MultiplayerReadyButton readyButton = new MultiplayerReadyButton
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

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.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
@ -68,6 +70,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved] [Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } private Bindable<RulesetInfo> ruleset { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private readonly IBindable<bool> operationInProgress = new BindableBool();
[CanBeNull]
private IDisposable applyingSettingsOperation;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -265,13 +275,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true);
MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true);
RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true);
operationInProgress.BindTo(ongoingOperationTracker.InProgress);
operationInProgress.BindValueChanged(v =>
{
if (v.NewValue)
loadingLayer.Show();
else
loadingLayer.Hide();
});
} }
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0; ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0 && !operationInProgress.Value;
} }
private void apply() private void apply()
@ -280,7 +299,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
return; return;
hideError(); hideError();
loadingLayer.Show();
Debug.Assert(applyingSettingsOperation == null);
applyingSettingsOperation = ongoingOperationTracker.BeginOperation();
// If the client is already in a room, update via the client. // If the client is already in a room, update via the client.
// Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation.
@ -313,16 +334,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void onSuccess(Room room) private void onSuccess(Room room)
{ {
loadingLayer.Hide(); Debug.Assert(applyingSettingsOperation != null);
SettingsApplied?.Invoke(); SettingsApplied?.Invoke();
applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
} }
private void onError(string text) private void onError(string text)
{ {
Debug.Assert(applyingSettingsOperation != null);
ErrorText.Text = text; ErrorText.Text = text;
ErrorText.FadeIn(50); ErrorText.FadeIn(50);
loadingLayer.Hide(); applyingSettingsOperation.Dispose();
applyingSettingsOperation = null;
} }
} }

View File

@ -1,15 +1,14 @@
// 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.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
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.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -24,15 +23,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
public Bindable<PlaylistItem> SelectedItem => button.SelectedItem; public Bindable<PlaylistItem> SelectedItem => button.SelectedItem;
public Action OnReadyClick
{
set => button.Action = value;
}
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[CanBeNull]
private MultiplayerRoomUser localUser;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private IBindable<bool> operationInProgress;
private SampleChannel sampleReadyCount; private SampleChannel sampleReadyCount;
private readonly ButtonWithTrianglesExposed button; private readonly ButtonWithTrianglesExposed button;
@ -46,7 +52,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Size = Vector2.One, Size = Vector2.One,
Enabled = { Value = true }, Enabled = { Value = true },
Action = onClick
}; };
} }
@ -54,21 +59,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty");
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
} }
protected override void OnRoomUpdated() protected override void OnRoomUpdated()
{ {
base.OnRoomUpdated(); base.OnRoomUpdated();
// this method is called on leaving the room, so the local user may not exist in the room any more.
localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id);
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open;
updateState(); updateState();
} }
private void updateState() private void updateState()
{ {
var localUser = Client.LocalUser;
if (localUser == null) if (localUser == null)
return; return;
@ -100,6 +106,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break; break;
} }
button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
if (newCountReady != countReady) if (newCountReady != countReady)
{ {
countReady = newCountReady; countReady = newCountReady;
@ -132,22 +140,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
} }
} }
private void onClick()
{
if (localUser == null)
return;
if (localUser.State == MultiplayerUserState.Idle)
Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true);
else
{
if (Room?.Host?.Equals(localUser) == true)
Client.StartMatch().CatchUnobservedExceptions(true);
else
Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true);
}
}
private class ButtonWithTrianglesExposed : ReadyButton private class ButtonWithTrianglesExposed : ReadyButton
{ {
public new Triangles Triangles => base.Triangles; public new Triangles Triangles => base.Triangles;

View File

@ -1,14 +1,17 @@
// 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.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; 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.Screens; using osu.Framework.Screens;
using osu.Game.Extensions;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
@ -31,10 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved] [Resolved]
private StatefulMultiplayerClient client { get; set; } private StatefulMultiplayerClient client { get; set; }
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private MultiplayerMatchSettingsOverlay settingsOverlay; private MultiplayerMatchSettingsOverlay settingsOverlay;
private IBindable<bool> isConnected; private IBindable<bool> isConnected;
[CanBeNull]
private IDisposable readyClickOperation;
public MultiplayerMatchSubScreen(Room room) public MultiplayerMatchSubScreen(Room room)
{ {
Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Title = room.RoomID.Value == null ? "New room" : room.Name.Value;
@ -150,7 +159,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}, },
new Drawable[] new Drawable[]
{ {
new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem } } new MultiplayerMatchFooter
{
SelectedItem = { BindTarget = SelectedItem },
OnReadyClick = onReadyClick
}
} }
}, },
RowDimensions = new[] RowDimensions = new[]
@ -196,6 +209,44 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault();
private void onReadyClick()
{
Debug.Assert(readyClickOperation == null);
readyClickOperation = ongoingOperationTracker.BeginOperation();
if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready)
{
client.StartMatch()
.ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
{
t.CatchUnobservedExceptions(true); // will run immediately.
// gameplay was not started due to an exception; unblock button.
endOperation();
}
// gameplay is starting, the button will be unblocked on load requested.
});
return;
}
client.ToggleReady()
.ContinueWith(t =>
{
t.CatchUnobservedExceptions(true); // will run immediately.
endOperation();
});
void endOperation()
{
Debug.Assert(readyClickOperation != null);
readyClickOperation.Dispose();
readyClickOperation = null;
}
}
private void onLoadRequested() private void onLoadRequested()
{ {
Debug.Assert(client.Room != null); Debug.Assert(client.Room != null);
@ -203,6 +254,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); int[] userIds = client.CurrentMatchPlayingUserIds.ToArray();
StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds));
Debug.Assert(readyClickOperation != null);
readyClickOperation.Dispose();
readyClickOperation = null;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -0,0 +1,59 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// Utility class to track ongoing online operations' progress.
/// Can be used to disable interactivity while waiting for a response from online sources.
/// </summary>
public class OngoingOperationTracker : Component
{
/// <summary>
/// Whether there is an online operation in progress.
/// </summary>
public IBindable<bool> InProgress => inProgress;
private readonly Bindable<bool> inProgress = new BindableBool();
private LeasedBindable<bool> leasedInProgress;
public OngoingOperationTracker()
{
AlwaysPresent = true;
}
/// <summary>
/// Begins tracking a new online operation.
/// </summary>
/// <returns>
/// An <see cref="IDisposable"/> that will automatically mark the operation as ended on disposal.
/// </returns>
/// <exception cref="InvalidOperationException">An operation has already been started.</exception>
public IDisposable BeginOperation()
{
if (leasedInProgress != null)
throw new InvalidOperationException("Cannot begin operation while another is in progress.");
leasedInProgress = inProgress.BeginLease(true);
leasedInProgress.Value = true;
// for extra safety, marshal the end of operation back to the update thread if necessary.
return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false));
}
private void endOperation()
{
if (leasedInProgress == null)
throw new InvalidOperationException("Cannot end operation multiple times.");
leasedInProgress.Return();
leasedInProgress = null;
}
}
}

View File

@ -53,6 +53,9 @@ namespace osu.Game.Screens.OnlinePlay
[Cached] [Cached]
private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria()); private readonly Bindable<FilterCriteria> currentFilter = new Bindable<FilterCriteria>(new FilterCriteria());
[Cached]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved(CanBeNull = true)] [Resolved(CanBeNull = true)]
private MusicController music { get; set; } private MusicController music { get; set; }
@ -141,7 +144,8 @@ namespace osu.Game.Screens.OnlinePlay
}; };
button.Action = () => OpenNewRoom(); button.Action = () => OpenNewRoom();
}), }),
RoomManager = CreateRoomManager() RoomManager = CreateRoomManager(),
ongoingOperationTracker = new OngoingOperationTracker()
} }
}; };

View File

@ -143,7 +143,11 @@ namespace osu.Game.Screens
private void load(OsuGame osu, AudioManager audio) private void load(OsuGame osu, AudioManager audio)
{ {
sampleExit = audio.Samples.Get(@"UI/screen-back"); sampleExit = audio.Samples.Get(@"UI/screen-back");
}
protected override void LoadComplete()
{
base.LoadComplete();
Activity.Value ??= InitialActivity; Activity.Value ??= InitialActivity;
} }

View File

@ -936,7 +936,6 @@ namespace osu.Game.Screens.Select
Masking = false; Masking = false;
} }
// ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910)
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
{ {
UserScrolling = true; UserScrolling = true;

View File

@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -42,6 +43,8 @@ namespace osu.Game.Screens.Select
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod();
public override void OnResuming(IScreen last) public override void OnResuming(IScreen last)
{ {
base.OnResuming(last); base.OnResuming(last);
@ -50,10 +53,10 @@ namespace osu.Game.Screens.Select
if (removeAutoModOnResume) if (removeAutoModOnResume)
{ {
var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod()?.GetType(); var autoType = getAutoplayMod()?.GetType();
if (autoType != null) if (autoType != null)
ModSelect.DeselectTypes(new[] { autoType }, true); Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray();
removeAutoModOnResume = false; removeAutoModOnResume = false;
} }
@ -81,12 +84,9 @@ namespace osu.Game.Screens.Select
// Ctrl+Enter should start map with autoplay enabled. // Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{ {
var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); var autoplayMod = getAutoplayMod();
var autoType = auto?.GetType();
var mods = Mods.Value; if (autoplayMod == null)
if (autoType == null)
{ {
notifications?.Post(new SimpleNotification notifications?.Post(new SimpleNotification
{ {
@ -95,9 +95,11 @@ namespace osu.Game.Screens.Select
return false; return false;
} }
if (mods.All(m => m.GetType() != autoType)) var mods = Mods.Value;
if (mods.All(m => m.GetType() != autoplayMod.GetType()))
{ {
Mods.Value = mods.Append(auto).ToArray(); Mods.Value = mods.Append(autoplayMod).ToArray();
removeAutoModOnResume = true; removeAutoModOnResume = true;
} }
} }

View File

@ -23,6 +23,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached] [Cached]
public Bindable<FilterCriteria> Filter { get; } public Bindable<FilterCriteria> Filter { get; }
[Cached]
public OngoingOperationTracker OngoingOperationTracker { get; }
protected override Container<Drawable> Content => content; protected override Container<Drawable> Content => content;
private readonly TestMultiplayerRoomContainer content; private readonly TestMultiplayerRoomContainer content;
@ -36,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Client = content.Client; Client = content.Client;
RoomManager = content.RoomManager; RoomManager = content.RoomManager;
Filter = content.Filter; Filter = content.Filter;
OngoingOperationTracker = content.OngoingOperationTracker;
} }
[SetUp] [SetUp]

View File

@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached] [Cached]
public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria()); public readonly Bindable<FilterCriteria> Filter = new Bindable<FilterCriteria>(new FilterCriteria());
[Cached]
public readonly OngoingOperationTracker OngoingOperationTracker;
public TestMultiplayerRoomContainer() public TestMultiplayerRoomContainer()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -33,6 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Client = new TestMultiplayerClient(), Client = new TestMultiplayerClient(),
RoomManager = new TestMultiplayerRoomManager(), RoomManager = new TestMultiplayerRoomManager(),
OngoingOperationTracker = new OngoingOperationTracker(),
content = new Container { RelativeSizeAxes = Axes.Both } content = new Container { RelativeSizeAxes = Axes.Both }
}); });
} }

View File

@ -26,7 +26,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1229.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.106.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="Sentry" Version="2.1.8" /> <PackageReference Include="Sentry" Version="2.1.8" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1229.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2021.106.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -88,7 +88,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1229.0" /> <PackageReference Include="ppy.osu.Framework" Version="2021.106.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" /> <PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" /> <PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" /> <PackageReference Include="SharpRaven" Version="2.4.0" />