1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-15 06:42:56 +08:00

Merge branch 'master' into user-beatmap-downloading-states

This commit is contained in:
Salman Ahmed 2021-01-13 22:31:31 +03:00 committed by GitHub
commit 560b1e970c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
144 changed files with 1943 additions and 1364 deletions

View File

@ -18,7 +18,7 @@
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.2" PrivateAssets="All" />
<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>
<PropertyGroup Label="Code Analysis">
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet>
@ -28,9 +28,17 @@
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<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 -->
<NoWarn>$(NoWarn);NU1701</NoWarn>
<!--
NU1701:
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 Label="Nuget">
<IsPackable>false</IsPackable>
@ -40,7 +48,7 @@
<RepositoryUrl>https://github.com/ppy/osu</RepositoryUrl>
<PackageReleaseNotes>Automated release.</PackageReleaseNotes>
<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>
</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
of this software and associated documentation files (the "Software"), to deal

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<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>
</Project>

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Users;
@ -31,13 +32,15 @@ namespace osu.Desktop
private readonly IBindable<UserStatus> status = new Bindable<UserStatus>();
private readonly IBindable<UserActivity> activity = new Bindable<UserActivity>();
private readonly Bindable<DiscordRichPresenceMode> privacyMode = new Bindable<DiscordRichPresenceMode>();
private readonly RichPresence presence = new RichPresence
{
Assets = new Assets { LargeImageKey = "osu_logo_lazer", }
};
[BackgroundDependencyLoader]
private void load(IAPIProvider provider)
private void load(IAPIProvider provider, OsuConfigManager config)
{
client = new DiscordRpcClient(client_id)
{
@ -51,6 +54,8 @@ namespace osu.Desktop
client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network);
config.BindWith(OsuSetting.DiscordRichPresence, privacyMode);
(user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u =>
{
status.UnbindBindings();
@ -63,6 +68,7 @@ namespace osu.Desktop
ruleset.BindValueChanged(_ => updateStatus());
status.BindValueChanged(_ => updateStatus());
activity.BindValueChanged(_ => updateStatus());
privacyMode.BindValueChanged(_ => updateStatus());
client.Initialize();
}
@ -78,7 +84,7 @@ namespace osu.Desktop
if (!client.IsInitialized)
return;
if (status.Value is UserStatusOffline)
if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off)
{
client.ClearPresence();
return;
@ -96,7 +102,10 @@ namespace osu.Desktop
}
// update user information
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
if (privacyMode.Value == DiscordRichPresenceMode.Limited)
presence.Assets.LargeImageText = string.Empty;
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty);
// update ruleset
presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom";
@ -137,7 +146,7 @@ namespace osu.Desktop
return edit.Beatmap.ToString();
case UserActivity.InLobby lobby:
return lobby.Room.Name.Value;
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}
return string.Empty;

View File

@ -26,9 +26,11 @@ namespace osu.Desktop.Overlays
Alpha = 0;
FillFlowContainer mainFill;
Children = new Drawable[]
{
new FillFlowContainer
mainFill = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
@ -55,23 +57,30 @@ namespace osu.Desktop.Overlays
},
}
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Numeric.With(size: 12),
Colour = colours.Yellow,
Text = @"Development Build"
},
new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
},
}
}
};
if (!game.IsDeployedBuild)
{
mainFill.AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Numeric.With(size: 12),
Colour = colours.Yellow,
Text = @"Development Build"
},
new Sprite
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Texture = textures.Get(@"Menu/dev-build-footer"),
},
});
}
}
protected override void PopIn()

View File

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

View File

@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Catch.Mods
{
base.ApplySettings(difficulty);
difficulty.CircleSize = CircleSize.Value;
difficulty.ApproachRate = ApproachRate.Value;
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
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.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Tests.Mods
@ -18,8 +22,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
public void TestNoAdjustment() => CreateModTest(new ModTestData
{
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,
PassCondition = checkSomeHit
PassCondition = () => checkSomeHit() && checkObjectsScale(0.29f)
});
[Test]

View File

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

View File

@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("sample-to-type-conversions")]
[TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")]
[TestCase("slider-generating-drumroll-2")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
}
}
private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing)
private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int taikoDuration, out double tickSpacing)
{
// DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS.
// Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable.
@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
// The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll.
double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate;
taikoDuration = distance / taikoVelocity * beatLength;
taikoDuration = (int)(distance / taikoVelocity * beatLength);
if (isForCurrentRuleset)
{
@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans);
tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans);
return tickSpacing > 0
&& distance / osuVelocity * 1000 < 2 * beatLength;

View File

@ -1,7 +1,9 @@
{
"Mappings": [{
"Mappings": [
{
"StartTime": 2000,
"Objects": [{
"Objects": [
{
"StartTime": 2000,
"EndTime": 2000,
"IsRim": false,
@ -23,7 +25,8 @@
},
{
"StartTime": 4000,
"Objects": [{
"Objects": [
{
"StartTime": 4000,
"EndTime": 4000,
"IsRim": false,
@ -45,7 +48,8 @@
},
{
"StartTime": 6000,
"Objects": [{
"Objects": [
{
"StartTime": 6000,
"EndTime": 6000,
"IsRim": true,
@ -76,300 +80,13 @@
},
{
"StartTime": 8000,
"Objects": [{
"Objects": [
{
"StartTime": 8000,
"EndTime": 8000,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8026,
"EndTime": 8026,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8053,
"EndTime": 8053,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8080,
"EndTime": 8080,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8107,
"EndTime": 8107,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8133,
"EndTime": 8133,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8160,
"EndTime": 8160,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8187,
"EndTime": 8187,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8214,
"EndTime": 8214,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8241,
"EndTime": 8241,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8267,
"EndTime": 8267,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8294,
"EndTime": 8294,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8321,
"EndTime": 8321,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8348,
"EndTime": 8348,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8374,
"EndTime": 8374,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8401,
"EndTime": 8401,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8428,
"EndTime": 8428,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8455,
"EndTime": 8455,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8482,
"EndTime": 8482,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8508,
"EndTime": 8508,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8535,
"EndTime": 8535,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8562,
"EndTime": 8562,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8589,
"EndTime": 8589,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8615,
"EndTime": 8615,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8642,
"EndTime": 8642,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8669,
"EndTime": 8669,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8696,
"EndTime": 8696,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8723,
"EndTime": 8723,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8749,
"EndTime": 8749,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8776,
"EndTime": 8776,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8803,
"EndTime": 8803,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8830,
"EndTime": 8830,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsSwell": false,
"IsStrong": false
},
{
"StartTime": 8857,
"EndTime": 8857,
"IsRim": false,
"IsCentre": true,
"IsDrumRoll": false,
"IsCentre": false,
"IsDrumRoll": true,
"IsSwell": false,
"IsStrong": false
}

View File

@ -0,0 +1,18 @@
{
"Mappings": [
{
"StartTime": 51532,
"Objects": [
{
"StartTime": 51532,
"EndTime": 52301,
"IsRim": false,
"IsCentre": false,
"IsDrumRoll": true,
"IsSwell": false,
"IsStrong": false
}
]
}
]
}

View File

@ -0,0 +1,19 @@
osu file format v14
[General]
Mode: 0
[Difficulty]
HPDrainRate:2
CircleSize:3.2
OverallDifficulty:2
ApproachRate:3
SliderMultiplier:0.999999999999999
SliderTickRate:1
[TimingPoints]
763,384.615384615385,4,2,0,70,1,0
49993,-90.9090909090909,4,2,0,75,0,1
[HitObjects]
51,245,51532,2,0,P|18:150|17:122,2,110.000003356934,0|8|0,0:0|0:0|0:0,0:0:0:0:

View File

@ -95,6 +95,26 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestOutOfOrderStartTimes()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("out-of-order-starttimes.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(2, background.Elements.Count);
Assert.AreEqual(1500, background.Elements[0].StartTime);
Assert.AreEqual(1000, background.Elements[1].StartTime);
Assert.AreEqual(1000, storyboard.EarliestEventTime);
}
}
[Test]
public void TestDecodeVariableWithSuffix()
{

View File

@ -246,5 +246,32 @@ namespace osu.Game.Tests.NonVisual
Assert.That(cpi.DifficultyPoints.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,6 @@
[Events]
//Storyboard Layer 0 (Background)
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1500,1600,0,1
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1000,1100,0,1

View File

@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Background
});
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur));
}
/// <summary>
@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.Background
public void TestStoryboardBackgroundVisibility()
{
performFullSetup();
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
createFakeStoryboard();
AddStep("Enable Storyboard", () =>
{
@ -198,8 +199,9 @@ namespace osu.Game.Tests.Visual.Background
})));
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
AddUntilStep("Screen is undimmed, original background retained", () =>
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect());
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur));
}
/// <summary>
@ -224,7 +226,7 @@ namespace osu.Game.Tests.Visual.Background
AddStep("Resume PlayerLoader", () => player.Restart());
AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur));
}
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
@ -274,9 +276,11 @@ namespace osu.Game.Tests.Visual.Background
private class DummySongSelect : PlaySongSelect
{
private FadeAccessibleBackground background;
protected override BackgroundScreen CreateBackground()
{
FadeAccessibleBackground background = new FadeAccessibleBackground(Beatmap.Value);
background = new FadeAccessibleBackground(Beatmap.Value);
DimEnabled.BindTo(background.EnableUserDim);
return background;
}
@ -294,25 +298,27 @@ namespace osu.Game.Tests.Visual.Background
config.BindWith(OsuSetting.BlurLevel, BlurLevel);
}
public bool IsBackgroundDimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1f - ((FadeAccessibleBackground)Background).CurrentDim);
public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim);
public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White;
public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White;
public bool IsUserBlurApplied() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR);
public bool IsUserBlurApplied() => background.CurrentBlur == new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR);
public bool IsUserBlurDisabled() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0);
public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0);
public bool IsBackgroundInvisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 0;
public bool IsBackgroundInvisible() => background.CurrentAlpha == 0;
public bool IsBackgroundVisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 1;
public bool IsBackgroundVisible() => background.CurrentAlpha == 1;
public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool IsBlurCorrect() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR);
public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected;
/// <summary>
/// Make sure every time a screen gets pushed, the background doesn't get replaced
/// </summary>
/// <returns>Whether or not the original background (The one created in DummySongSelect) is still the current background</returns>
public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen();
public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true;
}
private class FadeAccessibleResults : ResultsScreen
@ -324,12 +330,20 @@ namespace osu.Game.Tests.Visual.Background
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
public Vector2 ExpectedBackgroundBlur => new Vector2(BACKGROUND_BLUR);
}
private class LoadBlockingTestPlayer : TestPlayer
{
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
protected override BackgroundScreen CreateBackground() =>
new FadeAccessibleBackground(Beatmap.Value);
public override void OnEntering(IScreen last)
{
base.OnEntering(last);
ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground));
}
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
@ -354,15 +368,16 @@ namespace osu.Game.Tests.Visual.Background
Thread.Sleep(1);
StoryboardEnabled = config.GetBindable<bool>(OsuSetting.ShowStoryboard);
ReplacesBackground.BindTo(Background.StoryboardReplacesBackground);
DrawableRuleset.IsPaused.BindTo(IsPaused);
}
}
private class TestPlayerLoader : PlayerLoader
{
private FadeAccessibleBackground background;
public VisualSettings VisualSettingsPos => VisualSettings;
public BackgroundScreen ScreenPos => Background;
public BackgroundScreen ScreenPos => background;
public TestPlayerLoader(Player player)
: base(() => player)
@ -371,9 +386,9 @@ namespace osu.Game.Tests.Visual.Background
public void TriggerOnHover() => OnHover(new HoverEvent(new InputState()));
public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR);
public Vector2 ExpectedBackgroundBlur => new Vector2(BACKGROUND_BLUR);
protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value);
protected override BackgroundScreen CreateBackground() => background = new FadeAccessibleBackground(Beatmap.Value);
}
private class FadeAccessibleBackground : BackgroundScreenBeatmap

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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Beatmaps;
@ -18,6 +20,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
private MultiplayerMatchSubScreen screen;
[Cached]
private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker();
public TestSceneMultiplayerMatchSubScreen()
: base(false)
{

View File

@ -1,14 +1,17 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -23,10 +26,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private BeatmapSetInfo importedSet;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private IDisposable readyClickOperation;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
@ -38,9 +44,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
Child = button = new MultiplayerReadyButton
{
@ -51,13 +56,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Value = new PlaylistItem
{
Beatmap = { Value = beatmap },
Ruleset = { Value = beatmap.Ruleset }
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
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();
}
};
});
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddAssert("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddAssert("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddAssert("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{
@ -89,8 +124,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addClickButtonStep();
AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready);
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
verifyGameplayStartFlow();
}
[Test]
@ -105,8 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
addClickButtonStep();
AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0));
addClickButtonStep();
AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
verifyGameplayStartFlow();
}
[Test]
@ -160,5 +193,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(button);
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.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.Details;
using osuTK.Graphics;
@ -141,16 +142,12 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select changed Difficulty Adjust mod", () =>
{
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 adjustedDifficulty = new BeatmapDifficulty
{
CircleSize = originalDifficulty.CircleSize,
DrainRate = originalDifficulty.DrainRate - 0.5f,
OverallDifficulty = originalDifficulty.OverallDifficulty,
ApproachRate = originalDifficulty.ApproachRate + 2.2f,
};
difficultyAdjustMod.ReadFromDifficulty(adjustedDifficulty);
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
SelectedMods.Value = new[] { difficultyAdjustMod };
});

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
@ -14,8 +15,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneLoadingLayer : OsuTestScene
{
private Drawable dimContent;
private LoadingLayer overlay;
private TestLoadingLayer overlay;
private Container content;
@ -29,14 +29,14 @@ namespace osu.Game.Tests.Visual.UserInterface
Size = new Vector2(300),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
Children = new Drawable[]
{
new Box
{
Colour = Color4.SlateGray,
RelativeSizeAxes = Axes.Both,
},
dimContent = new FillFlowContainer
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
new TriangleButton { Text = "puush me", Width = 200, Action = () => { } },
}
},
overlay = new LoadingLayer(dimContent),
overlay = new TestLoadingLayer(true),
}
},
};
@ -64,25 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => overlay.Show());
AddUntilStep("wait for content dim", () => dimContent.Colour != Color4.White);
AddUntilStep("wait for content dim", () => overlay.BackgroundDimLayer.Alpha > 0);
AddStep("hide", () => overlay.Hide());
AddUntilStep("wait for content restore", () => dimContent.Colour == Color4.White);
}
[Test]
public void TestContentRestoreOnDispose()
{
AddAssert("not visible", () => !overlay.IsPresent);
AddStep("show", () => overlay.Show());
AddUntilStep("wait for content dim", () => dimContent.Colour != Color4.White);
AddStep("expire", () => overlay.Expire());
AddUntilStep("wait for content restore", () => dimContent.Colour == Color4.White);
AddUntilStep("wait for content restore", () => Precision.AlmostEquals(overlay.BackgroundDimLayer.Alpha, 0));
}
[Test]
@ -98,5 +84,15 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("hide", () => overlay.Hide());
}
private class TestLoadingLayer : LoadingLayer
{
public new Box BackgroundDimLayer => base.BackgroundDimLayer;
public TestLoadingLayer(bool dimBackground = false, bool withBox = true)
: base(dimBackground, withBox)
{
}
}
}
}

View File

@ -131,6 +131,18 @@ namespace osu.Game.Tests.Visual.UserInterface
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)
{
selectNext(mod);

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Tournament.Screens;
using osu.Game.Tournament.Screens.Setup;
namespace osu.Game.Tournament.Tests.Screens
{

View File

@ -1,7 +1,7 @@
// 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 osu.Game.Tournament.Screens;
using osu.Game.Tournament.Screens.Setup;
namespace osu.Game.Tournament.Tests.Screens
{

View File

@ -1,10 +1,12 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IO;
using System.IO;
using System.Collections.Generic;
using osu.Game.Tournament.Configuration;
namespace osu.Game.Tournament.IO
@ -13,25 +15,39 @@ namespace osu.Game.Tournament.IO
{
private const string default_tournament = "default";
private readonly Storage storage;
private readonly Storage allTournaments;
private readonly TournamentStorageManager storageConfig;
public readonly Bindable<string> CurrentTournament;
public TournamentStorage(Storage storage)
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
{
this.storage = storage;
allTournaments = UnderlyingStorage;
storageConfig = new TournamentStorageManager(storage);
if (storage.Exists("tournament.ini"))
{
ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
ChangeTargetStorage(allTournaments.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
}
else
Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
Migrate(allTournaments.GetStorageForDirectory(default_tournament));
CurrentTournament = storageConfig.GetBindable<string>(StorageConfig.CurrentTournament);
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
CurrentTournament.BindValueChanged(updateTournament);
}
private void updateTournament(ValueChangedEvent<string> newTournament)
{
ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue));
Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty));
}
public IEnumerable<string> ListTournaments() => allTournaments.GetDirectories(string.Empty);
public override void Migrate(Storage newStorage)
{
// this migration only happens once on moving to the per-tournament storage system.

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

@ -0,0 +1,72 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Setup
{
internal class ActionableInfo : LabelledDrawable<Drawable>
{
protected OsuButton Button;
public ActionableInfo()
: base(true)
{
}
public string ButtonText
{
set => Button.Text = value;
}
public string Value
{
set => valueText.Text = value;
}
public bool Failing
{
set => valueText.Colour = value ? Color4.Red : Color4.White;
}
public Action Action;
private TournamentSpriteText valueText;
protected FillFlowContainer FlowContainer;
protected override Drawable CreateComponent() => new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
valueText = new TournamentSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
FlowContainer = new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
Button = new TriangleButton
{
Size = new Vector2(100, 40),
Action = () => Action?.Invoke()
}
}
}
}
};
}
}

View File

@ -0,0 +1,53 @@
// 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.Graphics;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament.Screens.Setup
{
internal class ResolutionSelector : ActionableInfo
{
private const int minimum_window_height = 480;
private const int maximum_window_height = 2160;
public new Action<int> Action;
private OsuNumberBox numberBox;
protected override Drawable CreateComponent()
{
var drawable = base.CreateComponent();
FlowContainer.Insert(-1, numberBox = new OsuNumberBox
{
Text = "1080",
Width = 100
});
base.Action = () =>
{
if (string.IsNullOrEmpty(numberBox.Text))
return;
// box contains text
if (!int.TryParse(numberBox.Text, out var number))
{
// at this point, the only reason we can arrive here is if the input number was too big to parse into an int
// so clamp to max allowed value
number = maximum_window_height;
}
else
{
number = Math.Clamp(number, minimum_window_height, maximum_window_height);
}
// in case number got clamped, reset number in numberBox
numberBox.Text = number.ToString();
Action?.Invoke(number);
};
return drawable;
}
}
}

View File

@ -1,7 +1,6 @@
// 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.Collections.Generic;
using System.Drawing;
using osu.Framework.Allocation;
@ -17,9 +16,8 @@ using osu.Game.Rulesets;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament.Screens
namespace osu.Game.Tournament.Screens.Setup
{
public class SetupScreen : TournamentScreen, IProvideVideo
{
@ -64,9 +62,6 @@ namespace osu.Game.Tournament.Screens
reload();
}
[Resolved]
private Framework.Game game { get; set; }
private void reload()
{
var fileBasedIpc = ipc as FileBasedIPC;
@ -111,6 +106,11 @@ namespace osu.Game.Tournament.Screens
Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset,
},
new TournamentSwitcher
{
Label = "Current tournament",
Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart to apply changes.",
},
resolution = new ResolutionSelector
{
Label = "Stream area resolution",
@ -151,108 +151,5 @@ namespace osu.Game.Tournament.Screens
Width = 0.5f,
};
}
private class ActionableInfo : LabelledDrawable<Drawable>
{
private OsuButton button;
public ActionableInfo()
: base(true)
{
}
public string ButtonText
{
set => button.Text = value;
}
public string Value
{
set => valueText.Text = value;
}
public bool Failing
{
set => valueText.Colour = value ? Color4.Red : Color4.White;
}
public Action Action;
private TournamentSpriteText valueText;
protected FillFlowContainer FlowContainer;
protected override Drawable CreateComponent() => new Container
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
valueText = new TournamentSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
FlowContainer = new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10, 0),
Children = new Drawable[]
{
button = new TriangleButton
{
Size = new Vector2(100, 40),
Action = () => Action?.Invoke()
}
}
}
}
};
}
private class ResolutionSelector : ActionableInfo
{
private const int minimum_window_height = 480;
private const int maximum_window_height = 2160;
public new Action<int> Action;
private OsuNumberBox numberBox;
protected override Drawable CreateComponent()
{
var drawable = base.CreateComponent();
FlowContainer.Insert(-1, numberBox = new OsuNumberBox
{
Text = "1080",
Width = 100
});
base.Action = () =>
{
if (string.IsNullOrEmpty(numberBox.Text))
return;
// box contains text
if (!int.TryParse(numberBox.Text, out var number))
{
// at this point, the only reason we can arrive here is if the input number was too big to parse into an int
// so clamp to max allowed value
number = maximum_window_height;
}
else
{
number = Math.Clamp(number, minimum_window_height, maximum_window_height);
}
// in case number got clamped, reset number in numberBox
numberBox.Text = number.ToString();
Action?.Invoke(number);
};
return drawable;
}
}
}
}

View File

@ -13,11 +13,11 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Components;
using osu.Game.Tournament.IPC;
using osuTK;
namespace osu.Game.Tournament.Screens
namespace osu.Game.Tournament.Screens.Setup
{
public class StablePathSelectScreen : TournamentScreen
{

View File

@ -0,0 +1,44 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.IO;
namespace osu.Game.Tournament.Screens.Setup
{
internal class TournamentSwitcher : ActionableInfo
{
private OsuDropdown<string> dropdown;
[Resolved]
private TournamentGameBase game { get; set; }
[BackgroundDependencyLoader]
private void load(TournamentStorage storage)
{
string startupTournament = storage.CurrentTournament.Value;
dropdown.Current = storage.CurrentTournament;
dropdown.Items = storage.ListTournaments();
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
Action = () => game.GracefullyExit();
ButtonText = "Close osu!";
}
protected override Drawable CreateComponent()
{
var drawable = base.CreateComponent();
FlowContainer.Insert(-1, dropdown = new OsuDropdown<string>
{
Width = 510
});
return drawable;
}
}
}

View File

@ -8,12 +8,12 @@ using Newtonsoft.Json;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.IO;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
using osu.Game.Users;
using osuTK.Input;
@ -40,6 +40,8 @@ namespace osu.Game.Tournament
Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly));
dependencies.CacheAs<Storage>(storage = new TournamentStorage(baseStorage));
dependencies.CacheAs(storage);
dependencies.Cache(new TournamentVideoResourceStore(storage));
Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage)));
@ -60,7 +62,7 @@ namespace osu.Game.Tournament
{
using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open))
using (var sr = new StreamReader(stream))
ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd());
ladder = JsonConvert.DeserializeObject<LadderInfo>(sr.ReadToEnd(), new JsonPointConverter());
}
ladder ??= new LadderInfo();
@ -251,6 +253,7 @@ namespace osu.Game.Tournament
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
}));
}
}

View File

@ -19,6 +19,7 @@ using osu.Game.Tournament.Screens.Gameplay;
using osu.Game.Tournament.Screens.Ladder;
using osu.Game.Tournament.Screens.MapPool;
using osu.Game.Tournament.Screens.Schedule;
using osu.Game.Tournament.Screens.Setup;
using osu.Game.Tournament.Screens.Showcase;
using osu.Game.Tournament.Screens.TeamIntro;
using osu.Game.Tournament.Screens.TeamWin;

View File

@ -50,7 +50,15 @@ namespace osu.Game.Beatmaps
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>

View File

@ -28,5 +28,21 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <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>
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;
}
}
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)
=> existing is DifficultyControlPoint existingDifficulty
&& 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
&& KiaiMode == existingEffect.KiaiMode
&& 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
&& SampleBank == existingSample.SampleBank
&& 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.
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,25 @@ namespace osu.Game.Beatmaps.Formats
/// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it.
/// DO NOT USE THIS UNLESS 100% SURE.
/// </summary>
public readonly float BpmMultiplier;
public double BpmMultiplier { get; private set; }
public LegacyDifficultyControlPoint(double beatLength)
: this()
{
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1;
}
public LegacyDifficultyControlPoint()
{
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 +204,13 @@ namespace osu.Game.Beatmaps.Formats
=> base.IsRedundant(existing)
&& existing is LegacySampleControlPoint existingSample
&& CustomSampleBank == existingSample.CustomSampleBank;
public override void CopyFrom(ControlPoint other)
{
base.CopyFrom(other);
CustomSampleBank = ((LegacySampleControlPoint)other).CustomSampleBank;
}
}
}
}

View File

@ -0,0 +1,17 @@
// 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.ComponentModel;
namespace osu.Game.Configuration
{
public enum DiscordRichPresenceMode
{
Off,
[Description("Hide identifiable information")]
Limited,
Full
}
}

View File

@ -1,4 +1,4 @@
// 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.
using System;
@ -138,6 +138,8 @@ namespace osu.Game.Configuration
Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
Set(OsuSetting.EditorWaveformOpacity, 1f);
}
@ -266,6 +268,7 @@ namespace osu.Game.Configuration
GameplayDisableWinKey,
SeasonalBackgroundMode,
EditorWaveformOpacity,
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
}
}

View File

@ -1,7 +1,9 @@
// 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 osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
namespace osu.Game.Configuration
{
@ -14,6 +16,7 @@ namespace osu.Game.Configuration
{
Set(Static.LoginOverlayDisplayed, false);
Set(Static.MutedAudioNotificationShownOnce, false);
Set(Static.LastHoverSoundPlaybackTime, (double?)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).
/// </summary>
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

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Threading.Tasks;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Extensions
@ -13,13 +17,19 @@ namespace osu.Game.Extensions
/// Avoids unobserved exceptions from being fired.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="logOnError">Whether errors should be logged as important, or silently ignored.</param>
public static void CatchUnobservedExceptions(this Task task, bool logOnError = false)
/// <param name="logAsError">
/// Whether errors should be logged as errors visible to users, or as debug messages.
/// Logging as debug will essentially silence the errors on non-release builds.
/// </param>
public static void CatchUnobservedExceptions(this Task task, bool logAsError = false)
{
task.ContinueWith(t =>
{
if (logOnError)
Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important);
Exception? exception = t.Exception?.AsSingular();
if (logAsError)
Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true);
else
Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug);
}, TaskContinuationOptions.NotOnRanToCompletion);
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Graphics.UserInterface
TooltipText = "Downloading...";
break;
case DownloadState.Downloaded:
case DownloadState.Importing:
background.FadeColour(colours.Yellow, 500, Easing.InOutExpo);
TooltipText = "Importing";
break;

View File

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

View File

@ -2,8 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;
@ -17,22 +18,32 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public class LoadingLayer : LoadingSpinner
{
private readonly Drawable dimTarget;
[CanBeNull]
protected Box BackgroundDimLayer { get; }
/// <summary>
/// Constuct a new loading spinner.
/// Construct a new loading spinner.
/// </summary>
/// <param name="dimTarget">An optional target to dim when displayed.</param>
/// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public LoadingLayer(Drawable dimTarget = null, bool withBox = true)
public LoadingLayer(bool dimBackground = false, bool withBox = true)
: base(withBox)
{
RelativeSizeAxes = Axes.Both;
Size = new Vector2(1);
this.dimTarget = dimTarget;
MainContents.RelativeSizeAxes = Axes.None;
if (dimBackground)
{
AddInternal(BackgroundDimLayer = new Box
{
Depth = float.MaxValue,
Colour = Color4.Black,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
});
}
}
public override bool HandleNonPositionalInput => false;
@ -56,31 +67,21 @@ namespace osu.Game.Graphics.UserInterface
protected override void PopIn()
{
dimTarget?.FadeColour(OsuColour.Gray(0.5f), TRANSITION_DURATION, Easing.OutQuint);
BackgroundDimLayer?.FadeTo(0.5f, TRANSITION_DURATION * 2, Easing.OutQuint);
base.PopIn();
}
protected override void PopOut()
{
dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint);
BackgroundDimLayer?.FadeOut(TRANSITION_DURATION, Easing.OutQuint);
base.PopOut();
}
protected override void Update()
{
base.Update();
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (State.Value == Visibility.Visible)
{
// ensure we don't leave the target in a bad state.
dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint);
}
}
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
@ -112,8 +112,8 @@ namespace osu.Game.Input.Bindings
[Description("Toggle settings")]
ToggleSettings,
[Description("Toggle osu!direct")]
ToggleDirect,
[Description("Toggle beatmap listing")]
ToggleBeatmapListing,
[Description("Increase volume")]
IncreaseVolume,

View File

@ -31,7 +31,12 @@ namespace osu.Game.Online.API
Acronym = mod.Acronym;
foreach (var (_, property) in mod.GetSettingsSourceProperties())
Settings.Add(property.Name.Underscore(), property.GetValue(mod));
{
var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), bindable);
}
}
public Mod ToMod(Ruleset ruleset)
@ -46,7 +51,7 @@ namespace osu.Game.Online.API
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
((IBindable)property.GetValue(resultMod)).Parse(settingValue);
resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
}
return resultMod;

View File

@ -7,7 +7,7 @@ namespace osu.Game.Online
{
NotDownloaded,
Downloading,
Downloaded,
Importing,
LocallyAvailable
}
}

View File

@ -106,7 +106,7 @@ namespace osu.Game.Online
{
if (attachedRequest.Progress == 1)
{
State.Value = DownloadState.Downloaded;
State.Value = DownloadState.Importing;
Progress.Value = 1;
}
else
@ -125,7 +125,7 @@ namespace osu.Game.Online
}
}
private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Downloaded);
private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Importing);
private void onRequestProgress(float progress) => Schedule(() => Progress.Value = progress);

View File

@ -65,6 +65,23 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
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]
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 ChangeSettings(MultiplayerRoomSettings settings);

View File

@ -151,11 +151,11 @@ namespace osu.Game
updateBlockingOverlayFade();
}
public void RemoveBlockingOverlay(OverlayContainer overlay)
public void RemoveBlockingOverlay(OverlayContainer overlay) => Schedule(() =>
{
visibleBlockingOverlays.Remove(overlay);
updateBlockingOverlayFade();
}
});
/// <summary>
/// Close all game-wide overlays.
@ -942,18 +942,6 @@ namespace osu.Game
return base.OnExiting();
}
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// Will keep persisting until an exit occurs (exit may be blocked multiple times).
/// </summary>
public void GracefullyExit()
{
if (!OnExiting())
Exit();
else
Scheduler.AddDelayed(GracefullyExit, 2000);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();

View File

@ -380,6 +380,18 @@ namespace osu.Game
: new OsuConfigManager(Storage);
}
/// <summary>
/// Use to programatically exit the game as if the user was triggering via alt-f4.
/// Will keep persisting until an exit occurs (exit may be blocked multiple times).
/// </summary>
public void GracefullyExit()
{
if (!OnExiting())
Exit();
else
Scheduler.AddDelayed(GracefullyExit, 2000);
}
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();

View File

@ -48,11 +48,9 @@ namespace osu.Game.Overlays.AccountCreation
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
FillFlowContainer mainContent;
InternalChildren = new Drawable[]
{
mainContent = new FillFlowContainer
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
@ -124,7 +122,7 @@ namespace osu.Game.Overlays.AccountCreation
},
},
},
loadingLayer = new LoadingLayer(mainContent)
loadingLayer = new LoadingLayer(true)
};
textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox };

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private Container content;
public PreviewTrack Preview => PlayButton.Preview;
public Bindable<bool> PreviewPlaying => PlayButton?.Playing;
public IBindable<bool> PreviewPlaying => PlayButton?.Playing;
protected abstract PlayButton PlayButton { get; }
protected abstract Box PreviewBar { get; }

View File

@ -57,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
switch (State.Value)
{
case DownloadState.Downloading:
case DownloadState.Downloaded:
case DownloadState.Importing:
shakeContainer.Shake();
break;

View File

@ -50,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
progressBar.ResizeHeightTo(4, 400, Easing.OutQuint);
break;
case DownloadState.Downloaded:
case DownloadState.Importing:
progressBar.FadeIn(400, Easing.OutQuint);
progressBar.ResizeHeightTo(4, 400, Easing.OutQuint);

View File

@ -18,7 +18,10 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
{
public class PlayButton : Container
{
public readonly BindableBool Playing = new BindableBool();
public IBindable<bool> Playing => playing;
private readonly BindableBool playing = new BindableBool();
public PreviewTrack Preview { get; private set; }
private BeatmapSetInfo beatmapSet;
@ -36,7 +39,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
Preview?.Expire();
Preview = null;
Playing.Value = false;
playing.Value = false;
}
}
@ -82,7 +85,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
},
});
Playing.ValueChanged += playingStateChanged;
playing.ValueChanged += playingStateChanged;
}
[Resolved]
@ -96,7 +99,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
protected override bool OnClick(ClickEvent e)
{
Playing.Toggle();
playing.Toggle();
return true;
}
@ -108,7 +111,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
protected override void OnHoverLost(HoverLostEvent e)
{
if (!Playing.Value)
if (!playing.Value)
icon.FadeColour(Color4.White, 120, Easing.InOutQuint);
base.OnHoverLost(e);
}
@ -122,7 +125,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
{
if (BeatmapSet == null)
{
Playing.Value = false;
playing.Value = false;
return;
}
@ -142,10 +145,12 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
AddInternal(preview);
loading = false;
preview.Stopped += () => Playing.Value = false;
// make sure that the update of value of Playing (and the ensuing value change callbacks)
// are marshaled back to the update thread.
preview.Stopped += () => Schedule(() => playing.Value = false);
// user may have changed their mind.
if (Playing.Value)
if (playing.Value)
attemptStart();
});
}
@ -159,13 +164,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private void attemptStart()
{
if (Preview?.Start() != true)
Playing.Value = false;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Playing.Value = false;
playing.Value = false;
}
}
}

View File

@ -92,14 +92,14 @@ namespace osu.Game.Overlays
{
foundContent = new FillFlowContainer<BeatmapPanel>(),
notFoundContent = new NotFoundDrawable(),
loadingLayer = new LoadingLayer(panelTarget)
}
}
}
},
},
}
}
}
},
},
loadingLayer = new LoadingLayer(true)
};
}

View File

@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
Size = new Vector2(18),
Shadow = false,
},
loading = new LoadingLayer(icon, false),
loading = new LoadingLayer(true, false),
});
Action = () =>

View File

@ -126,7 +126,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
};
break;
case DownloadState.Downloaded:
case DownloadState.Importing:
textSprites.Children = new Drawable[]
{
new OsuSpriteText

View File

@ -18,13 +18,12 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
{
public class PreviewButton : OsuClickableContainer
{
private const float transition_duration = 500;
private readonly Box background, progress;
private readonly PlayButton playButton;
private PreviewTrack preview => playButton.Preview;
public Bindable<bool> Playing => playButton.Playing;
public IBindable<bool> Playing => playButton.Playing;
public BeatmapSetInfo BeatmapSet
{

View File

@ -287,7 +287,7 @@ namespace osu.Game.Overlays.BeatmapSet
break;
case DownloadState.Downloading:
case DownloadState.Downloaded:
case DownloadState.Importing:
// temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design.
downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value);
break;

View File

@ -157,11 +157,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
},
loading = new LoadingLayer()
}
}
}
}
},
},
loading = new LoadingLayer()
});
}
@ -228,7 +228,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
Scores = null;
notSupporterPlaceholder.Show();
loading.Hide();
loading.FinishTransforms();
return;
}
@ -241,6 +243,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
getScoresRequest.Success += scores =>
{
loading.Hide();
loading.FinishTransforms();
Scores = scores;
if (!scores.Scores.Any())

View File

@ -128,7 +128,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 50 }
},
loading = new LoadingLayer(itemsPlaceholder)
loading = new LoadingLayer(true)
}
}
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Overlays
}
}
},
loading = new LoadingLayer(content),
loading = new LoadingLayer(true),
};
}

View File

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

View File

@ -127,20 +127,30 @@ namespace osu.Game.Overlays.Mods
}
/// <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>
/// <param name="modTypes">The types of <see cref="Mod"/>s which should be selected.</param>
public void SelectTypes(IEnumerable<Type> modTypes)
/// <param name="newSelectedMods">The new list of selected mods to select.</param>
public void UpdateSelectedMods(IReadOnlyList<Mod> newSelectedMods)
{
foreach (var button in buttons)
{
int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType()));
updateButtonMods(button, newSelectedMods);
}
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()

View File

@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Mods
{
Width = 180,
Text = "Deselect All",
Action = DeselectAll,
Action = deselectAll,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
@ -318,7 +318,7 @@ namespace osu.Game.Overlays.Mods
sampleOff = audio.Samples.Get(@"UI/check-off");
}
public void DeselectAll()
private void deselectAll()
{
foreach (var section in ModSectionsContainer.Children)
section.DeselectAll();
@ -331,7 +331,7 @@ namespace osu.Game.Overlays.Mods
/// </summary>
/// <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>
public void DeselectTypes(Type[] modTypes, bool immediate = false)
private void deselectTypes(Type[] modTypes, bool immediate = false)
{
if (modTypes.Length == 0) return;
@ -409,7 +409,7 @@ namespace osu.Game.Overlays.Mods
private void selectedModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
foreach (var section in ModSectionsContainer.Children)
section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList());
section.UpdateSelectedMods(mods.NewValue);
updateMods();
}
@ -438,7 +438,7 @@ namespace osu.Game.Overlays.Mods
{
if (State.Value == Visibility.Visible) sampleOn?.Play();
DeselectTypes(selectedMod.IncompatibleMods, true);
deselectTypes(selectedMod.IncompatibleMods, true);
if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Overlays
},
},
},
loading = new LoadingLayer(content),
loading = new LoadingLayer(true),
};
}

View File

@ -45,6 +45,7 @@ namespace osu.Game.Overlays.Rankings
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Rankings
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Vertical = 10 }
},
loading = new LoadingLayer(content)
loading = new LoadingLayer(true)
}
}
}

View File

@ -42,6 +42,8 @@ namespace osu.Game.Overlays
Depth = -float.MaxValue
})
{
loading = new LoadingLayer(true);
Children = new Drawable[]
{
background = new Box
@ -74,12 +76,12 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Bottom = 10 }
},
loading = new LoadingLayer(contentContainer),
}
}
}
}
}
},
loading
};
}

View File

@ -132,6 +132,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
windowModes.BindCollectionChanged((sender, args) =>
{
@ -141,8 +150,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Hide();
}, true);
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown();
currentDisplay.BindValueChanged(display => Schedule(() =>
{
resolutions.RemoveRange(1, resolutions.Count - 1);
@ -159,8 +166,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
updateResolutionDropdown();
}), true);
scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
scalingMode.BindValueChanged(mode =>
{
scalingSettings.ClearTransforms();
@ -181,11 +186,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
/// <summary>
/// Create a delayed bindable which only updates when a condition is met.
/// </summary>
/// <param name="bindable">The config bindable.</param>
/// <returns>A bindable which will propagate updates with a delay.</returns>
private void bindPreviewEvent(Bindable<float> bindable)
{
bindable.ValueChanged += _ =>

View File

@ -0,0 +1,27 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Settings.Sections.Online
{
public class IntegrationSettings : SettingsSubsection
{
protected override string Header => "Integrations";
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
Children = new Drawable[]
{
new SettingsEnumDropdown<DiscordRichPresenceMode>
{
LabelText = "Discord Rich Presence",
Current = config.GetBindable<DiscordRichPresenceMode>(OsuSetting.DiscordRichPresence)
}
};
}
}
}

View File

@ -20,7 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections
{
Children = new Drawable[]
{
new WebSettings()
new WebSettings(),
new IntegrationSettings()
};
}
}

View File

@ -2,15 +2,18 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarBeatmapListingButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarBeatmapListingButton()
{
Hotkey = GlobalAction.ToggleDirect;
Hotkey = GlobalAction.ToggleBeatmapListing;
}
[BackgroundDependencyLoader(true)]

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarChangelogButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(ChangelogOverlay changelog)
{

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarChatButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarChatButton()
{
Hotkey = GlobalAction.ToggleChat;

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarNewsButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(NewsOverlay news)
{

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarRankingsButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(RankingsOverlay rankings)
{

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarSocialButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarSocialButton()
{
Hotkey = GlobalAction.ToggleSocial;

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
@ -84,12 +83,10 @@ namespace osu.Game.Rulesets.Mods
foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties())
{
object bindableObj = property.GetValue(this);
var bindable = (IBindable)property.GetValue(this);
if ((bindableObj as IHasDefaultValue)?.IsDefault == true)
continue;
tooltipTexts.Add($"{attr.Label} {bindableObj}");
if (!bindable.IsDefault)
tooltipTexts.Add($"{attr.Label} {bindable}");
}
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
@ -131,22 +128,50 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
public virtual Mod CreateCopy()
{
var copy = (Mod)Activator.CreateInstance(GetType());
var result = (Mod)Activator.CreateInstance(GetType());
result.CopyFrom(this);
return result;
}
/// <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));
// Copy bindable values across
foreach (var (_, prop) in this.GetSettingsSourceProperties())
{
var origBindable = prop.GetValue(this);
var copyBindable = prop.GetValue(copy);
var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source);
// The bindables themselves are readonly, so the value must be transferred through the Bindable<T>.Value property.
var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable<object>.Value), BindingFlags.Public | BindingFlags.Instance);
Debug.Assert(valueProperty != null);
valueProperty.SetValue(copyBindable, valueProperty.GetValue(origBindable));
// we only care about changes that have been made away from defaults.
if (!sourceBindable.IsDefault)
CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
return copy;
/// <summary>
/// When creating copies or clones of a Mod, this method will be called
/// to copy explicitly adjusted user settings from <paramref name="target"/>.
/// The base implementation will transfer the value via <see cref="Bindable{T}.Parse"/>
/// or by binding and unbinding (if <paramref name="source"/> is an <see cref="IBindable"/>)
/// and should be called unless replaced with custom logic.
/// </summary>
/// <param name="target">The target bindable to apply the adjustment to.</param>
/// <param name="source">The adjustment to apply.</param>
internal virtual void CopyAdjustedSetting(IBindable target, object source)
{
if (source is IBindable sourceBindable)
{
// copy including transfer of default values.
target.BindTo(sourceBindable);
target.UnbindFrom(sourceBindable);
}
else
target.Parse(source);
}
public bool Equals(IMod other) => GetType() == other?.GetType();

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods
public void ApplyToPlayer(Player player)
{
player.Background.EnableUserDim.Value = false;
player.ApplyToBackground(b => b.EnableUserDim.Value = false);
player.DimmableStoryboard.IgnoreUserSettings.Value = true;

View File

@ -114,14 +114,32 @@ namespace osu.Game.Rulesets.Mods
bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
}
internal override void CopyAdjustedSetting(IBindable target, object source)
{
// 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);
}
/// <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>
/// Apply all custom settings to the provided beatmap.
/// </summary>
/// <param name="difficulty">The beatmap to have settings applied.</param>
protected virtual void ApplySettings(BeatmapDifficulty difficulty)
{
difficulty.DrainRate = DrainRate.Value;
difficulty.OverallDifficulty = OverallDifficulty.Value;
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
}
}
}

View File

@ -750,7 +750,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
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"
+ $"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
{
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset,
#pragma warning disable 618
IHasCurve
#pragma warning restore 618
internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset
{
/// <summary>
/// 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>
/// A HitObject that ends at a different time than its start time.
/// </summary>
#pragma warning disable 618
public interface IHasDuration : IHasEndTime
#pragma warning restore 618
public interface IHasDuration
{
double IHasEndTime.EndTime
{
get => EndTime;
set => Duration = (Duration - EndTime) + value;
}
double IHasEndTime.Duration => Duration;
/// <summary>
/// The time at which the HitObject ends.
/// </summary>
new double EndTime { get; }
double EndTime { get; }
/// <summary>
/// The duration of the HitObject.
/// </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)))
{
// todo: StartsWith can be changed to Equals on 2020-11-08
// 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)
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}

View File

@ -34,6 +34,12 @@ namespace osu.Game.Screens
return false;
}
/// <summary>
/// Apply arbitrary changes to this background in a thread safe manner.
/// </summary>
/// <param name="action">The operation to perform.</param>
public void ApplyToBackground(Action<BackgroundScreen> action) => Schedule(() => action.Invoke(this));
protected override void Update()
{
base.Update();

View File

@ -16,6 +16,9 @@ namespace osu.Game.Screens.Edit.Compose
{
public class ComposeScreen : EditorScreenWithTimeline
{
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
private HitObjectComposer composer;
public ComposeScreen()
@ -59,7 +62,7 @@ namespace osu.Game.Screens.Edit.Compose
{
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
// full access to all skin sources.

View File

@ -444,11 +444,14 @@ namespace osu.Game.Screens.Edit
{
base.OnEntering(last);
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
Background.FadeColour(Color4.DarkGray, 500);
ApplyToBackground(b =>
{
// todo: temporary. we want to be applying dim using the UserDimContainer eventually.
b.FadeColour(Color4.DarkGray, 500);
Background.EnableUserDim.Value = false;
Background.BlurAmount.Value = 0;
b.EnableUserDim.Value = false;
b.BlurAmount.Value = 0;
});
resetTrack(true);
}
@ -480,9 +483,11 @@ namespace osu.Game.Screens.Edit
}
}
Background.FadeColour(Color4.White, 500);
ApplyToBackground(b => b.FadeColour(Color4.White, 500));
resetTrack();
Beatmap.Value = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo);
return base.OnExiting(next);
}

View File

@ -2,10 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Edit
{
@ -14,9 +12,6 @@ namespace osu.Game.Screens.Edit
/// </summary>
public abstract class EditorScreen : Container
{
[Resolved]
protected IBindable<WorkingBeatmap> Beatmap { get; private set; }
[Resolved]
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)]
private void load([CanBeNull] BindableBeatDivisor beatDivisor)
{
if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor);
Container mainContent;
LoadingSpinner spinner;
Children = new Drawable[]
{
mainContent = new Container
@ -99,6 +99,11 @@ namespace osu.Game.Screens.Edit
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(CreateMainContent(), content =>
{

View File

@ -13,9 +13,6 @@ namespace osu.Game.Screens.Edit.Setup
{
internal class DifficultySection : SetupSection
{
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
private LabelledSliderBar<float> circleSizeSlider;
private LabelledSliderBar<float> healthDrainSlider;
private LabelledSliderBar<float> approachRateSlider;
@ -34,7 +31,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = "Object Size",
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,
MinValue = 0,
@ -46,7 +43,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = "Health Drain",
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,
MinValue = 0,
@ -58,7 +55,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = "Approach Rate",
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,
MinValue = 0,
@ -70,7 +67,7 @@ namespace osu.Game.Screens.Edit.Setup
{
Label = "Overall Difficulty",
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,
MinValue = 0,
@ -88,12 +85,12 @@ namespace osu.Game.Screens.Edit.Setup
{
// 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.
Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value;
Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
editorBeatmap.UpdateAllHitObjects();
Beatmap.UpdateAllHitObjects();
}
}
}

Some files were not shown because too many files have changed in this diff Show More