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

Merge branch 'master' into scaling

This commit is contained in:
Bartłomiej Dach 2024-05-23 15:10:40 +02:00
commit 8a2dd4d816
No known key found for this signature in database
81 changed files with 937 additions and 327 deletions

View File

@ -21,7 +21,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2023.1117.0",
"version": "2024.517.0",
"commands": [
"localisation"
]

View File

@ -2,7 +2,6 @@
<Project>
<PropertyGroup Label="C#">
<LangVersion>12.0</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup>

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.509.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.523.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -164,8 +164,8 @@ namespace osu.Desktop
// user activity
if (activity.Value != null)
{
presence.State = truncate(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = truncate(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0)
{
@ -271,8 +271,19 @@ namespace osu.Desktop
private static readonly int ellipsis_length = Encoding.UTF8.GetByteCount(new[] { '…' });
private static string truncate(string str)
private static string clampLength(string str)
{
// Empty strings are fine to discord even though single-character strings are not. Make it make sense.
if (string.IsNullOrEmpty(str))
return str;
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end.
if (str.Length < 2)
return str.PadRight(2, '\u200B');
if (Encoding.UTF8.GetByteCount(str) <= 128)
return str;

View File

@ -3,15 +3,12 @@
#nullable disable
using System;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -62,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.UI
onSkinChanged();
}
protected override void LoadComplete()
{
base.LoadComplete();
updateMobileSizing();
}
private void onSkinChanged()
{
for (int i = 0; i < stageDefinition.Columns; i++)
@ -92,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.UI
columns[i].Width = width.Value;
}
updateMobileSizing();
}
/// <summary>
@ -106,31 +95,6 @@ namespace osu.Game.Rulesets.Mania.UI
Content[column] = columns[column].Child = content;
}
private void updateMobileSizing()
{
if (!IsLoaded || !RuntimeInfo.IsMobile)
return;
// GridContainer+CellContainer containing this stage (gets split up for dual stages).
Vector2? containingCell = this.FindClosestParent<Stage>()?.Parent?.DrawSize;
// Will be null in tests.
if (containingCell == null)
return;
float aspectRatio = containingCell.Value.X / containingCell.Value.Y;
// 2.83 is a mostly arbitrary scale-up (170 / 60, based on original implementation for argon)
float mobileAdjust = 2.83f * Math.Min(1, 7f / stageDefinition.Columns);
// 1.92 is a "reference" mobile screen aspect ratio for phones.
// We should scale it back for cases like tablets which aren't so extreme.
mobileAdjust *= aspectRatio / 1.92f;
// Best effort until we have better mobile support.
for (int i = 0; i < stageDefinition.Columns; i++)
columns[i].Width *= mobileAdjust;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);

View File

@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
rhythmStart++;
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
@ -66,10 +67,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
}
else
{
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
@ -100,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
startRatio = effectiveRatio;
islandSize = 1;
}
lastObj = prevObj;
prevObj = currObj;
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)

View File

@ -8,9 +8,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public readonly PathControlPoint ControlPoint;
private readonly T hitObject;
private readonly Container marker;
private readonly Circle circle;
private readonly Drawable markerRing;
[Resolved]
@ -60,38 +60,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
marker = new Container
circle = new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Children = new[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(20),
},
markerRing = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(28),
Masking = true,
BorderThickness = 2,
BorderColour = Color4.White,
Alpha = 0,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
Size = new Vector2(20),
},
markerRing = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(28),
Alpha = 0,
InnerRadius = 0.1f,
Progress = 1
}
};
}
@ -115,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
// The connecting path is excluded from positional input
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => marker.ReceivePositionalInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => circle.ReceivePositionalInputAt(screenSpacePos);
protected override bool OnHover(HoverEvent e)
{
@ -209,8 +193,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1);
marker.Colour = colour;
marker.Scale = new Vector2(hitObject.Scale);
Colour = colour;
Scale = new Vector2(hitObject.Scale);
}
private Color4 getColourFromNodeType()

View File

@ -26,12 +26,5 @@ namespace osu.Game.Rulesets.Taiko.Edit
ShowSpeedChanges.BindValueChanged(showChanges => VisualisationMethod = showChanges.NewValue ? ScrollVisualisationMethod.Overlapping : ScrollVisualisationMethod.Constant, true);
}
protected override double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = ShowSpeedChanges.Value ? 1 : 4;
return base.ComputeTimeRange() / multiplier;
}
}
}

View File

@ -0,0 +1,29 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModConstantSpeed : Mod, IApplicableToDrawableRuleset<TaikoHitObject>
{
public override string Name => "Constant Speed";
public override string Acronym => "CS";
public override double ScoreMultiplier => 0.9;
public override LocalisableString Description => "No more tricky speed changes!";
public override IconUsage? Icon => FontAwesome.Solid.Equals;
public override ModType Type => ModType.Conversion;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var taikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
taikoRuleset.VisualisationMethod = ScrollVisualisationMethod.Constant;
}
}
}

View File

@ -150,6 +150,7 @@ namespace osu.Game.Rulesets.Taiko
new TaikoModClassic(),
new TaikoModSwap(),
new TaikoModSingleTap(),
new TaikoModConstantSpeed(),
};
case ModType.Automation:

View File

@ -82,7 +82,12 @@ namespace osu.Game.Rulesets.Taiko.UI
TimeRange.Value = ComputeTimeRange();
}
protected virtual double ComputeTimeRange() => PlayfieldAdjustmentContainer.ComputeTimeRange();
protected virtual double ComputeTimeRange()
{
// Adjust when we're using constant algorithm to not be sluggish.
double multiplier = VisualisationMethod == ScrollVisualisationMethod.Constant ? 4 * Beatmap.Difficulty.SliderMultiplier : 1;
return PlayfieldAdjustmentContainer.ComputeTimeRange() / multiplier;
}
protected override void UpdateAfterChildren()
{

View File

@ -1188,5 +1188,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(beatmap.HitObjects[0].GetEndTime(), Is.EqualTo(3153));
}
}
[Test]
public void TestBeatmapDifficultyIsClamped()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.DrainRate, Is.EqualTo(10));
Assert.That(decoded.CircleSize, Is.EqualTo(10));
Assert.That(decoded.OverallDifficulty, Is.EqualTo(10));
Assert.That(decoded.ApproachRate, Is.EqualTo(10));
Assert.That(decoded.SliderMultiplier, Is.EqualTo(3.6));
Assert.That(decoded.SliderTickRate, Is.EqualTo(8));
}
}
[Test]
public void TestManiaBeatmapDifficultyCircleSizeClamp()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("out-of-range-difficulties-mania.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream).Difficulty;
Assert.That(decoded.CircleSize, Is.EqualTo(14));
}
}
}
}

View File

@ -0,0 +1,5 @@
[General]
Mode: 3
[Difficulty]
CircleSize:14

View File

@ -0,0 +1,10 @@
[General]
Mode: 0
[Difficulty]
HPDrainRate:25
CircleSize:25
OverallDifficulty:25
ApproachRate:30
SliderMultiplier:30
SliderTickRate:30

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Collections.Generic;
using Humanizer;
using NUnit.Framework;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@ -396,7 +397,7 @@ namespace osu.Game.Tests.Visual.Editing
textBox.Current.Value = bank;
// force a commit via keyboard.
// this is needed when testing attempting to set empty bank - which should revert to the previous value, but only on commit.
InputManager.ChangeFocus(textBox);
((IFocusManager)InputManager).ChangeFocus(textBox);
InputManager.Key(Key.Enter);
});

View File

@ -6,6 +6,7 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Timing;
using osu.Game.Graphics.UserInterface;
@ -62,12 +63,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 7", () => numeratorTextBox.Current.Value = "7");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 7/4", () => timeSignature.Current.Value.Equals(new TimeSignature(7)));
}
@ -77,12 +78,12 @@ namespace osu.Game.Tests.Visual.Editing
createLabelledTimeSignature(TimeSignature.SimpleQuadruple);
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("focus text box", () => InputManager.ChangeFocus(numeratorTextBox));
AddStep("focus text box", () => ((IFocusManager)InputManager).ChangeFocus(numeratorTextBox));
AddStep("set numerator to 0", () => numeratorTextBox.Current.Value = "0");
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddStep("drop focus", () => InputManager.ChangeFocus(null));
AddStep("drop focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("current is 4/4", () => timeSignature.Current.Value.Equals(TimeSignature.SimpleQuadruple));
AddAssert("numerator is 4", () => numeratorTextBox.Current.Value == "4");
}

View File

@ -1,8 +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.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
@ -26,7 +24,7 @@ namespace osu.Game.Tests.Visual.Navigation
{
public partial class TestScenePresentScore : OsuGameTestScene
{
private BeatmapSetInfo beatmap;
private BeatmapSetInfo beatmap = null!;
[SetUpSteps]
public new void SetUpSteps()
@ -64,7 +62,7 @@ namespace osu.Game.Tests.Visual.Navigation
Ruleset = new OsuRuleset().RulesetInfo
},
}
})?.Value;
})!.Value;
});
}
@ -158,6 +156,27 @@ namespace osu.Game.Tests.Visual.Navigation
presentAndConfirm(secondImport, type);
}
[Test]
public void TestScoreRefetchIgnoresEmptyHash()
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo?.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
importScore(-1, hash: string.Empty);
importScore(3, hash: @"deadbeef");
// oftentimes a `PresentScore()` call will be given a `ScoreInfo` which is converted from an online score,
// in which cases the hash will generally not be available.
AddStep("present score", () => Game.PresentScore(new ScoreInfo { OnlineID = 3, Hash = string.Empty }));
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
AddUntilStep("correct score displayed", () =>
{
var score = ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score!;
return score.OnlineID == 3 && score.Hash == "deadbeef";
});
}
private void returnToMenu()
{
// if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track).
@ -171,14 +190,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
}
private Func<ScoreInfo> importScore(int i, RulesetInfo ruleset = null)
private Func<ScoreInfo> importScore(int i, RulesetInfo? ruleset = null, string? hash = null)
{
ScoreInfo imported = null;
ScoreInfo? imported = null;
AddStep($"import score {i}", () =>
{
imported = Game.ScoreManager.Import(new ScoreInfo
{
Hash = Guid.NewGuid().ToString(),
Hash = hash ?? Guid.NewGuid().ToString(),
OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo,
@ -188,14 +207,14 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert($"import {i} succeeded", () => imported != null);
return () => imported;
return () => imported!;
}
/// <summary>
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
/// </summary>
private IScreen lastWaitedScreen;
private IScreen lastWaitedScreen = null!;
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
{

View File

@ -0,0 +1,83 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Screens.Menu;
using osuTK.Input;
using Color4 = osuTK.Graphics.Color4;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneMainMenuButton : OsuTestScene
{
[Resolved]
private MetadataClient metadataClient { get; set; } = null!;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
[Test]
public void TestStandardButton()
{
AddStep("add button", () => Child = new MainMenuButton(
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
}
[Test]
public void TestDailyChallengeButton()
{
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{
switch (req)
{
case GetRoomRequest getRoomRequest:
if (getRoomRequest.RoomId != 1234)
return false;
var beatmap = CreateAPIBeatmap();
beatmap.OnlineID = 1001;
getRoomRequest.TriggerSuccess(new Room
{
RoomID = { Value = 1234 },
Playlist =
{
new PlaylistItem(beatmap)
},
EndDate = { Value = DateTimeOffset.Now.AddSeconds(30) }
});
return true;
default:
return false;
}
});
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -623,7 +624,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);
AddStep("unfocus search text box externally", () => InputManager.ChangeFocus(null));
AddStep("unfocus search text box externally", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddStep("press tab", () => InputManager.Key(Key.Tab));
AddAssert("search text box focused", () => modSelectOverlay.SearchTextBox.HasFocus);

View File

@ -15,15 +15,16 @@ using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.FooterV2;
using osu.Game.Screens.Footer;
using osu.Game.Screens.SelectV2.Footer;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
{
private FooterButtonRandomV2 randomButton = null!;
private FooterButtonModsV2 modsButton = null!;
private ScreenFooterButtonRandom randomButton = null!;
private ScreenFooterButtonMods modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
@ -39,25 +40,25 @@ namespace osu.Game.Tests.Visual.SongSelect
nextRandomCalled = false;
previousRandomCalled = false;
FooterV2 footer;
ScreenFooter footer;
Children = new Drawable[]
{
new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = footer = new FooterV2(),
Child = footer = new ScreenFooter(),
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new FooterButtonModsV2 { Current = SelectedMods }, overlay);
footer.AddButton(randomButton = new FooterButtonRandomV2
footer.AddButton(modsButton = new ScreenFooterButtonMods { Current = SelectedMods }, overlay);
footer.AddButton(randomButton = new ScreenFooterButtonRandom
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
});
footer.AddButton(new FooterButtonOptionsV2());
footer.AddButton(new ScreenFooterButtonOptions());
overlay.Hide();
});
@ -98,7 +99,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
AddStep("enable options", () =>
{
var optionsButton = this.ChildrenOfType<FooterButtonV2>().Last();
var optionsButton = this.ChildrenOfType<ScreenFooterButton>().Last();
optionsButton.Enabled.Value = true;
optionsButton.TriggerClick();
@ -108,7 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[Test]
public void TestState()
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<FooterButtonV2>().Last().Enabled.Value = state);
AddToggleStep("set options enabled state", state => this.ChildrenOfType<ScreenFooterButton>().Last().Enabled.Value = state);
}
[Test]

View File

@ -12,21 +12,21 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Select.FooterV2;
using osu.Game.Screens.SelectV2.Footer;
using osu.Game.Utils;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFooterButtonModsV2 : OsuTestScene
public partial class TestSceneScreenFooterButtonMods : OsuTestScene
{
private readonly TestFooterButtonModsV2 footerButtonMods;
private readonly TestScreenFooterButtonMods footerButtonMods;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
public TestSceneFooterButtonModsV2()
public TestSceneScreenFooterButtonMods()
{
Add(footerButtonMods = new TestFooterButtonModsV2
Add(footerButtonMods = new TestScreenFooterButtonMods
{
Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
@ -97,9 +97,9 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestUnrankedBadge()
{
AddStep(@"Add unranked mod", () => changeMods(new[] { new OsuModDeflate() }));
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<FooterButtonModsV2.UnrankedBadge>().Single().Alpha == 1);
AddUntilStep("Unranked badge shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 1);
AddStep(@"Clear selected mod", () => changeMods(Array.Empty<Mod>()));
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<FooterButtonModsV2.UnrankedBadge>().Single().Alpha == 0);
AddUntilStep("Unranked badge not shown", () => footerButtonMods.ChildrenOfType<ScreenFooterButtonMods.UnrankedBadge>().Single().Alpha == 0);
}
private void changeMods(IReadOnlyList<Mod> mods) => footerButtonMods.Current.Value = mods;
@ -112,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface
return expectedValue == footerButtonMods.MultiplierText.Current.Value;
}
private partial class TestFooterButtonModsV2 : FooterButtonModsV2
private partial class TestScreenFooterButtonMods : ScreenFooterButtonMods
{
public new OsuSpriteText MultiplierText => base.MultiplierText;
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
@ -42,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("set instantaneous to false", () => sliderWithTextBoxInput.Instantaneous = false);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.Zero);
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.Zero);
@ -61,7 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -71,12 +72,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -87,7 +88,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("set instantaneous to true", () => sliderWithTextBoxInput.Instantaneous = true);
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("change text", () => textBox.Text = "3");
AddAssert("slider moved", () => slider.Current.Value, () => Is.EqualTo(3));
AddAssert("current changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(3));
@ -106,7 +107,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("textbox not changed", () => textBox.Current.Value, () => Is.EqualTo("-5"));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
@ -116,12 +117,12 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("focus textbox", () => InputManager.ChangeFocus(textBox));
AddStep("focus textbox", () => ((IFocusManager)InputManager).ChangeFocus(textBox));
AddStep("set text to invalid", () => textBox.Text = "garbage");
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));
AddStep("lose focus", () => InputManager.ChangeFocus(null));
AddStep("lose focus", () => ((IFocusManager)InputManager).ChangeFocus(null));
AddAssert("text restored", () => textBox.Text, () => Is.EqualTo("-5"));
AddAssert("slider not moved", () => slider.Current.Value, () => Is.EqualTo(-5));
AddAssert("current not changed", () => sliderWithTextBoxInput.Current.Value, () => Is.EqualTo(-5));

View File

@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
editorInfo.Selected.ValueChanged += selection =>
{
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null);
GetContainingFocusManager().TriggerFocusContention(null);
// Required to avoid cyclic failure in BindableWithCurrent (TriggerChange called during the Current_Set process).
// Arguable a framework issue but since we haven't hit it anywhere else a local workaround seems best.

View File

@ -27,8 +27,17 @@ namespace osu.Game.Beatmaps.Drawables
set => base.Masking = value;
}
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
protected override double LoadDelay { get; }
private readonly double timeBeforeUnload;
protected override double TransformDuration => 400;
public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover, double timeBeforeLoad = 500, double timeBeforeUnload = 1000)
{
LoadDelay = timeBeforeLoad;
this.timeBeforeUnload = timeBeforeUnload;
this.coverType = coverType;
InternalChild = new Box
@ -38,12 +47,12 @@ namespace osu.Game.Beatmaps.Drawables
};
}
protected override double LoadDelay => 500;
protected override double TransformDuration => 400;
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, timeBeforeUnload)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Formats
base.ParseStreamInto(stream, beatmap);
applyDifficultyRestrictions(beatmap.Difficulty, beatmap);
flushPendingPoints();
// Objects may be out of order *only* if a user has manually edited an .osu file.
@ -102,10 +104,30 @@ namespace osu.Game.Beatmaps.Formats
}
}
/// <summary>
/// Ensures that all <see cref="BeatmapDifficulty"/> settings are within the allowed ranges.
/// See also: https://github.com/peppy/osu-stable-reference/blob/0e425c0d525ef21353c8293c235cc0621d28338b/osu!/GameplayElements/Beatmaps/Beatmap.cs#L567-L614
/// </summary>
private static void applyDifficultyRestrictions(BeatmapDifficulty difficulty, Beatmap beatmap)
{
difficulty.DrainRate = Math.Clamp(difficulty.DrainRate, 0, 10);
// mania uses "circle size" for key count, thus different allowable range
difficulty.CircleSize = beatmap.BeatmapInfo.Ruleset.OnlineID != 3
? Math.Clamp(difficulty.CircleSize, 0, 10)
: Math.Clamp(difficulty.CircleSize, 1, 18);
difficulty.OverallDifficulty = Math.Clamp(difficulty.OverallDifficulty, 0, 10);
difficulty.ApproachRate = Math.Clamp(difficulty.ApproachRate, 0, 10);
difficulty.SliderMultiplier = Math.Clamp(difficulty.SliderMultiplier, 0.4, 3.6);
difficulty.SliderTickRate = Math.Clamp(difficulty.SliderTickRate, 0.5, 8);
}
/// <summary>
/// Processes the beatmap such that a new combo is started the first hitobject following each break.
/// </summary>
private void postProcessBreaks(Beatmap beatmap)
private static void postProcessBreaks(Beatmap beatmap)
{
int currentBreak = 0;
bool forceNewCombo = false;
@ -161,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats
/// This method's intention is to restore those legacy defaults.
/// See also: https://osu.ppy.sh/wiki/en/Client/File_formats/Osu_%28file_format%29
/// </summary>
private void applyLegacyDefaults(BeatmapInfo beatmapInfo)
private static void applyLegacyDefaults(BeatmapInfo beatmapInfo)
{
beatmapInfo.WidescreenStoryboard = false;
beatmapInfo.SamplesMatchPlaybackRate = false;
@ -402,11 +424,11 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SliderMultiplier":
difficulty.SliderMultiplier = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.4, 3.6);
difficulty.SliderMultiplier = Parsing.ParseDouble(pair.Value);
break;
case @"SliderTickRate":
difficulty.SliderTickRate = Math.Clamp(Parsing.ParseDouble(pair.Value), 0.5, 8);
difficulty.SliderTickRate = Parsing.ParseDouble(pair.Value);
break;
}
}

View File

@ -202,7 +202,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader]
private void load()
{
AddInternal(addOrRemoveButton = new IconButton
AddInternal(addOrRemoveButton = new NoFocusChangeIconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@ -271,6 +271,11 @@ namespace osu.Game.Collections
}
protected override Drawable CreateContent() => (Content)base.CreateContent();
private partial class NoFocusChangeIconButton : IconButton
{
public override bool ChangeFocusOnClick => false;
}
}
}
}

View File

@ -137,7 +137,7 @@ namespace osu.Game.Collections
this.ScaleTo(0.9f, exit_duration);
// Ensure that textboxes commit
GetContainingInputManager()?.TriggerFocusContention(this);
GetContainingFocusManager()?.TriggerFocusContention(this);
}
}
}

View File

@ -1134,7 +1134,17 @@ namespace osu.Game.Database
case 41:
foreach (var score in migration.NewRealm.All<ScoreInfo>())
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
{
try
{
// this can fail e.g. if a user has a score set on a ruleset that can no longer be loaded.
LegacyScoreDecoder.PopulateTotalScoreWithoutMods(score);
}
catch (Exception ex)
{
Logger.Log($@"Failed to populate total score without mods for score {score.ID}: {ex}", LoggingTarget.Database);
}
}
break;
}

View File

@ -120,6 +120,7 @@ namespace osu.Game.Graphics
public static IconUsage Cross => get(OsuIconMapping.Cross);
public static IconUsage CrossCircle => get(OsuIconMapping.CrossCircle);
public static IconUsage Crown => get(OsuIconMapping.Crown);
public static IconUsage DailyChallenge => get(OsuIconMapping.DailyChallenge);
public static IconUsage Debug => get(OsuIconMapping.Debug);
public static IconUsage Delete => get(OsuIconMapping.Delete);
public static IconUsage Details => get(OsuIconMapping.Details);
@ -218,6 +219,9 @@ namespace osu.Game.Graphics
[Description(@"crown")]
Crown,
[Description(@"daily-challenge")]
DailyChallenge,
[Description(@"debug")]
Debug,

View File

@ -31,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
if (!allowImmediateFocus)
return;
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(this));
Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(this));
}
public new void KillFocus() => base.KillFocus();

View File

@ -52,8 +52,6 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
const float nub_padding = 5;
Children = new Drawable[]
{
LabelTextFlowContainer = new OsuTextFlowContainer(ApplyLabelParameters)
@ -69,15 +67,13 @@ namespace osu.Game.Graphics.UserInterface
{
Nub.Anchor = Anchor.CentreRight;
Nub.Origin = Anchor.CentreRight;
Nub.Margin = new MarginPadding { Right = nub_padding };
LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 };
LabelTextFlowContainer.Padding = new MarginPadding { Right = Nub.DEFAULT_EXPANDED_SIZE + 10f };
}
else
{
Nub.Anchor = Anchor.CentreLeft;
Nub.Origin = Anchor.CentreLeft;
Nub.Margin = new MarginPadding { Left = nub_padding };
LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + nub_padding * 2 };
LabelTextFlowContainer.Padding = new MarginPadding { Left = Nub.DEFAULT_EXPANDED_SIZE + 10f };
}
Nub.Current.BindTo(Current);

View File

@ -57,7 +57,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
GetContainingInputManager().ChangeFocus(Component);
GetContainingFocusManager().ChangeFocus(Component);
}
protected override OsuTextBox CreateComponent() => CreateTextBox().With(t =>

View File

@ -85,7 +85,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
Current.BindValueChanged(updateTextBoxFromSlider, true);
}
public bool TakeFocus() => GetContainingInputManager().ChangeFocus(textBox);
public bool TakeFocus() => GetContainingFocusManager().ChangeFocus(textBox);
private bool updatingFromTextBox;

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 osu.Framework.Localisation;
@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"exit");
/// <summary>
/// "daily challenge"
/// </summary>
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "Are you sure you want to delete all scores? This cannot be undone!"
/// </summary>
public static LocalisableString Scores => new TranslatableString(getKey(@"collections"), @"Are you sure you want to delete all scores? This cannot be undone!");
public static LocalisableString Scores => new TranslatableString(getKey(@"scores"), @"Are you sure you want to delete all scores? This cannot be undone!");
/// <summary>
/// "Are you sure you want to delete all mod presets?"

View File

@ -47,7 +47,7 @@ namespace osu.Game.Localisation
public static LocalisableString Calculating => new TranslatableString(getKey(@"calculating"), @"calculating...");
/// <summary>
/// "{0} items"
/// "{0} item(s)"
/// </summary>
public static LocalisableString Items(int arg0) => new TranslatableString(getKey(@"items"), @"{0} item(s)", arg0);

View File

@ -114,7 +114,7 @@ Please try changing your audio device to a working setting.");
public static LocalisableString MismatchingBeatmapForReplay => new TranslatableString(getKey(@"mismatching_beatmap_for_replay"), @"Your local copy of the beatmap for this replay appears to be different than expected. You may need to update or re-download it.");
/// <summary>
/// "You are now running osu! {version}.
/// "You are now running osu! {0}.
/// Click to see what's new!"
/// </summary>
public static LocalisableString GameVersionAfterUpdate(string version) => new TranslatableString(getKey(@"game_version_after_update"), @"You are now running osu! {0}.

View File

@ -0,0 +1,16 @@
// 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 MessagePack;
namespace osu.Game.Online.Metadata
{
[MessagePackObject]
[Serializable]
public struct DailyChallengeInfo
{
[Key(0)]
public long RoomID { get; set; }
}
}

View File

@ -20,5 +20,11 @@ namespace osu.Game.Online.Metadata
/// Delivers an update of the <see cref="UserPresence"/> of the user with the supplied <paramref name="userId"/>.
/// </summary>
Task UserPresenceUpdated(int userId, UserPresence? status);
/// <summary>
/// Delivers an update of the current "daily challenge" status.
/// Null value means there is no "daily challenge" currently active.
/// </summary>
Task DailyChallengeUpdated(DailyChallengeInfo? info);
}
}

View File

@ -7,7 +7,12 @@ using osu.Game.Users;
namespace osu.Game.Online.Metadata
{
/// <summary>
/// Metadata server is responsible for keeping the osu! client up-to-date with any changes.
/// Metadata server is responsible for keeping the osu! client up-to-date with various real-time happenings, such as:
/// <list type="bullet">
/// <item>beatmap updates via BSS,</item>
/// <item>online user activity/status updates,</item>
/// <item>other real-time happenings, such as current "daily challenge" status.</item>
/// </list>
/// </summary>
public interface IMetadataServer
{

View File

@ -59,6 +59,15 @@ namespace osu.Game.Online.Metadata
#endregion
#region Daily Challenge
public abstract IBindable<DailyChallengeInfo?> DailyChallengeInfo { get; }
/// <inheritdoc/>
public abstract Task DailyChallengeUpdated(DailyChallengeInfo? info);
#endregion
#region Disconnection handling
public event Action? Disconnecting;

View File

@ -26,6 +26,9 @@ namespace osu.Game.Online.Metadata
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
private readonly string endpoint;
private IHubClientConnector? connector;
@ -58,6 +61,7 @@ namespace osu.Game.Online.Metadata
// https://github.com/dotnet/aspnetcore/issues/15198
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
};
@ -101,6 +105,7 @@ namespace osu.Game.Online.Metadata
{
isWatchingUserPresence.Value = false;
userStates.Clear();
dailyChallengeInfo.Value = null;
});
return;
}
@ -229,6 +234,12 @@ namespace osu.Game.Online.Metadata
}
}
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
{
Schedule(() => dailyChallengeInfo.Value = info);
return Task.CompletedTask;
}
public override async Task DisconnectRequested()
{
await base.DisconnectRequested().ConfigureAwait(false);

View File

@ -13,5 +13,8 @@ namespace osu.Game.Online.Rooms
[Description("Featured Artist")]
FeaturedArtist,
[Description("Daily Challenge")]
DailyChallenge,
}
}

View File

@ -578,17 +578,17 @@ namespace osu.Game
{
case ITabletHandler th:
return new TabletSettings(th);
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
}
}
switch (handler)
{
case MouseHandler mh:
return new MouseSettings(mh);
case JoystickHandler jh:
return new JoystickSettings(jh);
case TouchHandler th:
return new TouchSettings(th);

View File

@ -243,7 +243,7 @@ namespace osu.Game.Overlays.AccountCreation
if (nextTextBox != null)
{
Schedule(() => GetContainingInputManager().ChangeFocus(nextTextBox));
Schedule(() => GetContainingFocusManager().ChangeFocus(nextTextBox));
return true;
}

View File

@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Comments
base.LoadComplete();
if (!TextBox.ReadOnly)
GetContainingInputManager().ChangeFocus(TextBox);
GetContainingFocusManager().ChangeFocus(TextBox);
}
protected override void OnCommit(string text)

View File

@ -150,7 +150,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e)
{
Schedule(() => { GetContainingInputManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
Schedule(() => { GetContainingFocusManager().ChangeFocus(string.IsNullOrEmpty(username.Text) ? username : password); });
}
}
}

View File

@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Login
}
if (form != null)
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form));
ScheduleAfterChildren(() => GetContainingFocusManager()?.ChangeFocus(form));
});
private void updateDropdownCurrent(UserStatus? status)
@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e)
{
if (form != null) GetContainingInputManager().ChangeFocus(form);
if (form != null) GetContainingFocusManager().ChangeFocus(form);
base.OnFocus(e);
}
}

View File

@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Login
protected override void OnFocus(FocusEvent e)
{
Schedule(() => { GetContainingInputManager().ChangeFocus(codeTextBox); });
Schedule(() => { GetContainingFocusManager().ChangeFocus(codeTextBox); });
}
}
}

View File

@ -78,7 +78,7 @@ namespace osu.Game.Overlays
this.FadeIn(transition_time, Easing.OutQuint);
FadeEdgeEffectTo(WaveContainer.SHADOW_OPACITY, WaveContainer.APPEAR_DURATION, Easing.Out);
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(panel));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(panel));
}
protected override void PopOut()

View File

@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
nameTextBox.Current.BindValueChanged(s =>
{

View File

@ -136,7 +136,7 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(nameTextBox));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(nameTextBox));
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)

View File

@ -949,7 +949,7 @@ namespace osu.Game.Overlays.Mods
RequestScroll?.Invoke(this);
// Killing focus is done here because it's the only feasible place on ModSelectOverlay you can click on without triggering any action.
Scheduler.Add(() => GetContainingInputManager().ChangeFocus(null));
Scheduler.Add(() => GetContainingFocusManager().ChangeFocus(null));
return true;
}

View File

@ -465,7 +465,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
if (HasFocus)
GetContainingInputManager().ChangeFocus(null);
GetContainingFocusManager().ChangeFocus(null);
cancelAndClearButtons.FadeOut(300, Easing.OutQuint);
cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y;

View File

@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
var next = Children.SkipWhile(c => c != sender).Skip(1).FirstOrDefault();
if (next != null)
GetContainingInputManager().ChangeFocus(next);
GetContainingFocusManager().ChangeFocus(next);
}
}
}

View File

@ -105,12 +105,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input
highPrecisionMouse.Current.BindValueChanged(highPrecision =>
{
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
switch (RuntimeInfo.OS)
{
if (highPrecision.NewValue)
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else
highPrecisionMouse.ClearNoticeText();
case RuntimeInfo.Platform.Linux:
case RuntimeInfo.Platform.macOS:
case RuntimeInfo.Platform.iOS:
if (highPrecision.NewValue)
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else
highPrecisionMouse.ClearNoticeText();
break;
}
}, true);
}

View File

@ -201,7 +201,7 @@ namespace osu.Game.Overlays
searchTextBox.HoldFocus = false;
if (searchTextBox.HasFocus)
GetContainingInputManager().ChangeFocus(null);
GetContainingFocusManager().ChangeFocus(null);
}
public override bool AcceptsFocus => true;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
@ -284,13 +285,43 @@ namespace osu.Game.Overlays.SkinEditor
drawable.Position -= drawable.AnchorPosition - previousAnchor;
}
private static void applyOrigin(Drawable drawable, Anchor origin)
private static void applyOrigin(Drawable drawable, Anchor screenSpaceOrigin)
{
if (origin == drawable.Origin) return;
var boundingBox = drawable.ScreenSpaceDrawQuad.AABBFloat;
var previousOrigin = drawable.OriginPosition;
drawable.Origin = origin;
drawable.Position += drawable.OriginPosition - previousOrigin;
var targetScreenSpacePosition = screenSpaceOrigin.PositionOnQuad(boundingBox);
Anchor localOrigin = Anchor.TopLeft;
float smallestDistanceFromTargetPosition = float.PositiveInfinity;
void checkOrigin(Anchor originToTest)
{
Vector2 positionToTest = drawable.ToScreenSpace(originToTest.PositionOnQuad(drawable.DrawRectangle));
float testedDistance = Vector2.Distance(targetScreenSpacePosition, positionToTest);
if (testedDistance < smallestDistanceFromTargetPosition)
{
localOrigin = originToTest;
smallestDistanceFromTargetPosition = testedDistance;
}
}
checkOrigin(Anchor.TopLeft);
checkOrigin(Anchor.TopCentre);
checkOrigin(Anchor.TopRight);
checkOrigin(Anchor.CentreLeft);
checkOrigin(Anchor.Centre);
checkOrigin(Anchor.CentreRight);
checkOrigin(Anchor.BottomLeft);
checkOrigin(Anchor.BottomCentre);
checkOrigin(Anchor.BottomRight);
Vector2 offset = drawable.ToParentSpace(localOrigin.PositionOnQuad(drawable.DrawRectangle)) - drawable.ToParentSpace(drawable.Origin.PositionOnQuad(drawable.DrawRectangle));
drawable.Origin = localOrigin;
drawable.Position += offset;
}
}
}

View File

@ -88,7 +88,7 @@ namespace osu.Game.Scoring
{
ScoreInfo? databasedScoreInfo = null;
if (originalScoreInfo is ScoreInfo scoreInfo)
if (originalScoreInfo is ScoreInfo scoreInfo && !string.IsNullOrEmpty(scoreInfo.Hash))
databasedScoreInfo = Query(s => s.Hash == scoreInfo.Hash);
if (originalScoreInfo.OnlineID > 0)

View File

@ -580,7 +580,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
base.LoadComplete();
GetContainingInputManager().ChangeFocus(this);
GetContainingFocusManager().ChangeFocus(this);
SelectAll();
}
}

View File

@ -138,7 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(sliderVelocitySlider));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(sliderVelocitySlider));
}
}
}

View File

@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
protected override void LoadComplete()
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(volume));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(volume));
}
private static string? getCommonBank(IList<HitSampleInfo>[] relevantSamples) => relevantSamples.Select(GetBankValue).Distinct().Count() == 1 ? GetBankValue(relevantSamples.First()) : null;

View File

@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Setup
OnFocused?.Invoke();
base.OnFocus(e);
GetContainingInputManager().TriggerFocusContention(this);
GetContainingFocusManager().TriggerFocusContention(this);
}
}
}

View File

@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup
base.LoadComplete();
if (string.IsNullOrEmpty(ArtistTextBox.Current.Value))
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(ArtistTextBox));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(ArtistTextBox));
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));

View File

@ -126,7 +126,7 @@ namespace osu.Game.Screens.Edit.Timing
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
GetContainingInputManager().ChangeFocus(textBox);
GetContainingFocusManager().ChangeFocus(textBox);
}
private void updateState()

View File

@ -11,9 +11,9 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.Footer
{
public partial class FooterV2 : InputBlockingContainer
public partial class ScreenFooter : InputBlockingContainer
{
//Should be 60, setting to 50 for now for the sake of matching the current BackButton height.
private const int height = 50;
@ -23,7 +23,7 @@ namespace osu.Game.Screens.Select.FooterV2
/// <param name="button">The button to be added.</param>
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
public void AddButton(FooterButtonV2 button, OverlayContainer? overlay = null)
public void AddButton(ScreenFooterButton button, OverlayContainer? overlay = null)
{
if (overlay != null)
{
@ -46,9 +46,9 @@ namespace osu.Game.Screens.Select.FooterV2
}
}
private FillFlowContainer<FooterButtonV2> buttons = null!;
private FillFlowContainer<ScreenFooterButton> buttons = null!;
public FooterV2()
public ScreenFooter()
{
RelativeSizeAxes = Axes.X;
Height = height;
@ -66,7 +66,7 @@ namespace osu.Game.Screens.Select.FooterV2
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
buttons = new FillFlowContainer<FooterButtonV2>
buttons = new FillFlowContainer<ScreenFooterButton>
{
Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10),
Anchor = Anchor.BottomLeft,

View File

@ -21,9 +21,9 @@ using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.Footer
{
public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
public partial class ScreenFooterButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
// This should be 12 by design, but an extra allowance is added due to the corner radius specification.
private const float shear_width = 13.5f;
@ -70,7 +70,7 @@ namespace osu.Game.Screens.Select.FooterV2
private readonly Box glowBox;
private readonly Box flashLayer;
public FooterButtonV2()
public ScreenFooterButton()
{
Size = new Vector2(BUTTON_WIDTH, BUTTON_HEIGHT);

View File

@ -24,6 +24,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@ -46,6 +47,7 @@ namespace osu.Game.Screens.Menu
public Action? OnSettings;
public Action? OnMultiplayer;
public Action? OnPlaylists;
public Action<Room>? OnDailyChallenge;
private readonly IBindable<bool> isIdle = new BindableBool();
@ -102,10 +104,13 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[]
{
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O, Key.S),
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel,
-WEDGE_WIDTH)
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
},
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel)
{
Padding = new MarginPadding { Right = WEDGE_WIDTH },
VisibleStateMin = ButtonSystemState.Play,
VisibleStateMax = ButtonSystemState.Edit,
},
@ -127,21 +132,31 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-default-select", OsuIcon.Online, new Color4(94, 63, 186, 255), onMultiplayer, Key.M));
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-default-select", OsuIcon.Tournament, new Color4(94, 63, 186, 255), onPlaylists, Key.L));
buttonsPlay.Add(new DailyChallengeButton(@"button-default-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), () => OnEditBeatmap?.Invoke(), WEDGE_WIDTH, Key.B, Key.E));
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), () => OnEditSkin?.Invoke(), 0, Key.S));
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P, Key.M, Key.L));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => State = ButtonSystemState.Edit, 0, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.B, Key.D));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
{
Padding = new MarginPadding { Left = WEDGE_WIDTH },
});
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D));
if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q));
buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit);
@ -164,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
}
private void onMultiplayer()
private void onMultiplayer(MainMenuButton _)
{
if (api.State.Value != APIState.Online)
{
@ -175,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke();
}
private void onPlaylists()
private void onPlaylists(MainMenuButton _)
{
if (api.State.Value != APIState.Online)
{
@ -186,6 +201,20 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke();
}
private void onDailyChallenge(MainMenuButton button)
{
if (api.State.Value != APIState.Online)
{
loginOverlay?.Show();
return;
}
var dailyChallengeButton = (DailyChallengeButton)button;
if (dailyChallengeButton.Room != null)
OnDailyChallenge?.Invoke(dailyChallengeButton.Room);
}
private void updateIdleState(bool isIdle)
{
if (!ReturnToTopOnIdle)

View File

@ -0,0 +1,221 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Screens.Menu
{
public partial class DailyChallengeButton : MainMenuButton, IHasCustomTooltip<APIBeatmapSet?>
{
public Room? Room { get; private set; }
private readonly OsuSpriteText countdown;
private ScheduledDelegate? scheduledCountdownUpdate;
private UpdateableOnlineBeatmapSetCover cover = null!;
private IBindable<DailyChallengeInfo?> info = null!;
private Box gradientLayer = null!;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{
BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT);
Content.Add(countdown = new OsuSpriteText
{
Shadow = true,
AllowMultiline = false,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding
{
Left = -3,
Bottom = 22,
},
Font = OsuFont.Default.With(size: 12),
Alpha = 0,
});
}
protected override Drawable CreateBackground(Colour4 accentColour) => new BufferedContainer
{
Children = new Drawable[]
{
cover = new UpdateableOnlineBeatmapSetCover(timeBeforeLoad: 0, timeBeforeUnload: 600_000)
{
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativePositionAxes = Axes.Both,
},
gradientLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(accentColour.Opacity(0.2f), accentColour),
Blending = BlendingParameters.Additive,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = accentColour.Opacity(0.7f)
},
},
};
[BackgroundDependencyLoader]
private void load(MetadataClient metadataClient)
{
info = metadataClient.DailyChallengeInfo.GetBoundCopy();
}
protected override void LoadComplete()
{
base.LoadComplete();
info.BindValueChanged(updateDisplay, true);
}
protected override void Update()
{
base.Update();
if (cover.LatestTransformEndTime == Time.Current)
{
const double duration = 3000;
float scale = 1 + RNG.NextSingle();
cover.ScaleTo(scale, duration, Easing.InOutSine)
.RotateTo(RNG.NextSingle(-4, 4) * (scale - 1), duration, Easing.InOutSine)
.MoveTo(new Vector2(
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1),
RNG.NextSingle(-0.5f, 0.5f) * (scale - 1)
), duration, Easing.InOutSine);
gradientLayer.FadeIn(duration / 2)
.Then()
.FadeOut(duration / 2);
}
}
private void updateDisplay(ValueChangedEvent<DailyChallengeInfo?> info)
{
UpdateState();
scheduledCountdownUpdate?.Cancel();
scheduledCountdownUpdate = null;
if (info.NewValue == null)
{
Room = null;
cover.OnlineInfo = TooltipContent = null;
}
else
{
var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID);
roomRequest.Success += room =>
{
Room = room;
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true);
};
api.Queue(roomRequest);
}
}
private void updateCountdown()
{
if (Room == null)
return;
var remaining = (Room.EndDate.Value - DateTimeOffset.Now) ?? TimeSpan.Zero;
if (remaining <= TimeSpan.Zero)
{
countdown.FadeOut(250, Easing.OutQuint);
}
else
{
if (countdown.Alpha == 0)
countdown.FadeIn(250, Easing.OutQuint);
countdown.Text = remaining.ToString(@"hh\:mm\:ss");
}
}
protected override void UpdateState()
{
if (info.IsNotNull() && info.Value == null)
{
ContractStyle = 0;
State = ButtonState.Contracted;
return;
}
base.UpdateState();
}
public ITooltip<APIBeatmapSet?> GetCustomTooltip() => new DailyChallengeTooltip();
public APIBeatmapSet? TooltipContent { get; private set; }
internal partial class DailyChallengeTooltip : CompositeDrawable, ITooltip<APIBeatmapSet?>
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private APIBeatmapSet? lastContent;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
}
public void Move(Vector2 pos) => Position = pos;
public void SetContent(APIBeatmapSet? content)
{
if (content == lastContent)
return;
lastContent = content;
ClearInternal();
if (content != null)
AddInternal(new BeatmapCardNano(content));
}
}
}
}

View File

@ -147,6 +147,12 @@ namespace osu.Game.Screens.Menu
OnSolo = loadSoloSongSelect,
OnMultiplayer = () => this.Push(new Multiplayer()),
OnPlaylists = () => this.Push(new Playlists()),
OnDailyChallenge = room =>
{
Playlists playlistsScreen;
this.Push(playlistsScreen = new Playlists());
playlistsScreen.Join(room);
},
OnExit = () =>
{
exitConfirmedViaHoldOrClick = true;

View File

@ -38,11 +38,8 @@ namespace osu.Game.Screens.Menu
public readonly Key[] TriggerKeys;
private readonly Container iconText;
private readonly Container box;
private readonly Box boxHoverLayer;
private readonly SpriteIcon icon;
private readonly string sampleName;
protected override Container<Drawable> Content => content;
private readonly Container content;
/// <summary>
/// The menu state for which we are visible for (assuming only one).
@ -59,7 +56,24 @@ namespace osu.Game.Screens.Menu
public ButtonSystemState VisibleStateMin = ButtonSystemState.TopLevel;
public ButtonSystemState VisibleStateMax = ButtonSystemState.TopLevel;
private readonly Action? clickAction;
public new MarginPadding Padding
{
get => Content.Padding;
set => Content.Padding = value;
}
protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT);
private readonly Action<MainMenuButton>? clickAction;
private readonly Container background;
private readonly Drawable backgroundContent;
private readonly Box boxHoverLayer;
private readonly SpriteIcon icon;
private Vector2 initialSize => BaseSize + Padding.Total;
private readonly string sampleName;
private Sample? sampleClick;
private Sample? sampleHover;
private SampleChannel? sampleChannel;
@ -68,9 +82,9 @@ namespace osu.Game.Screens.Menu
// Allow keyboard interaction based on state rather than waiting for delayed animations.
|| state == ButtonState.Expanded;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos);
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action? clickAction = null, float extraWidth = 0, params Key[] triggerKeys)
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
{
this.sampleName = sampleName;
this.clickAction = clickAction;
@ -79,11 +93,9 @@ namespace osu.Game.Screens.Menu
AutoSizeAxes = Axes.Both;
Alpha = 0;
Vector2 boxSize = new Vector2(ButtonSystem.BUTTON_WIDTH + Math.Abs(extraWidth), ButtonArea.BUTTON_AREA_HEIGHT);
Children = new Drawable[]
AddRangeInternal(new Drawable[]
{
box = new Container
background = new Container
{
// box needs to be always present to ensure the button is always sized correctly for flow
AlwaysPresent = true,
@ -98,35 +110,46 @@ namespace osu.Game.Screens.Menu
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(0, 1),
Size = boxSize,
Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / boxSize.Y, 0),
Children = new[]
{
new Box
backgroundContent = CreateBackground(colour).With(bg =>
{
EdgeSmoothness = new Vector2(1.5f, 0),
RelativeSizeAxes = Axes.Both,
Colour = colour,
},
bg.RelativeSizeAxes = Axes.Y;
bg.X = -ButtonSystem.WEDGE_WIDTH;
bg.Anchor = Anchor.Centre;
bg.Origin = Anchor.Centre;
}),
boxHoverLayer = new Box
{
EdgeSmoothness = new Vector2(1.5f, 0),
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = Color4.White,
Depth = float.MinValue,
Alpha = 0,
},
}
},
iconText = new Container
content = new Container
{
AutoSizeAxes = Axes.Both,
Position = new Vector2(extraWidth / 2, 0),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new OsuSpriteText
{
Shadow = true,
AllowMultiline = false,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Margin = new MarginPadding
{
Left = -3,
Bottom = 7,
},
Text = text
},
icon = new SpriteIcon
{
Shadow = true,
@ -136,20 +159,38 @@ namespace osu.Game.Screens.Menu
Position = new Vector2(0, 0),
Margin = new MarginPadding { Top = -4 },
Icon = symbol
},
new OsuSpriteText
{
Shadow = true,
AllowMultiline = false,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(0, 35),
Margin = new MarginPadding { Left = -3 },
Text = text
}
}
}
};
});
}
protected virtual Drawable CreateBackground(Colour4 accentColour) => new Container
{
Child = new Box
{
EdgeSmoothness = new Vector2(1.5f, 0),
RelativeSizeAxes = Axes.Both,
Colour = accentColour,
}
};
protected override void LoadComplete()
{
base.LoadComplete();
background.Shear = new Vector2(ButtonSystem.WEDGE_WIDTH / initialSize.Y, 0);
// for whatever reason, attempting to size the background "just in time" to cover the visible width
// results in gaps when the width changes are quick (only visible when testing menu at 100% speed, not visible slowed down).
// to ensure there's no missing backdrop, just use a ballpark that should be enough to always cover the width and then some.
// note that while on a code inspections it would seem that `1.5 * initialSize.X` would be enough, elastic usings are used in this button
// (which can exceed the [0;1] range during interpolation).
backgroundContent.Width = 2 * initialSize.X;
backgroundContent.Shear = -background.Shear;
animateState();
FinishTransforms(true);
}
private bool rightward;
@ -179,15 +220,15 @@ namespace osu.Game.Screens.Menu
{
if (State != ButtonState.Expanded) return true;
sampleHover?.Play();
box.ScaleTo(new Vector2(1.5f, 1), 500, Easing.OutElastic);
double duration = TimeUntilNextBeat;
icon.ClearTransforms();
icon.RotateTo(rightward ? -BOUNCE_ROTATION : BOUNCE_ROTATION, duration, Easing.InOutSine);
icon.ScaleTo(new Vector2(HOVER_SCALE, HOVER_SCALE * BOUNCE_COMPRESSION), duration, Easing.Out);
sampleHover?.Play();
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(1.5f, 1)), 500, Easing.OutElastic);
return true;
}
@ -199,7 +240,7 @@ namespace osu.Game.Screens.Menu
icon.ScaleTo(Vector2.One, 200, Easing.Out);
if (State == ButtonState.Expanded)
box.ScaleTo(new Vector2(1, 1), 500, Easing.OutElastic);
background.ResizeTo(initialSize, 500, Easing.OutElastic);
}
[BackgroundDependencyLoader]
@ -246,7 +287,7 @@ namespace osu.Game.Screens.Menu
sampleChannel = sampleClick?.GetChannel();
sampleChannel?.Play();
clickAction?.Invoke();
clickAction?.Invoke(this);
boxHoverLayer.ClearTransforms();
boxHoverLayer.Alpha = 0.9f;
@ -254,13 +295,13 @@ namespace osu.Game.Screens.Menu
}
public override bool HandleNonPositionalInput => state == ButtonState.Expanded;
public override bool HandlePositionalInput => state != ButtonState.Exploded && box.Scale.X >= 0.8f;
public override bool HandlePositionalInput => state != ButtonState.Exploded && background.Width / initialSize.X >= 0.8f;
public void StopSamplePlayback() => sampleChannel?.Stop();
protected override void Update()
{
iconText.Alpha = Math.Clamp((box.Scale.X - 0.5f) / 0.3f, 0, 1);
content.Alpha = Math.Clamp((background.Width / initialSize.X - 0.5f) / 0.3f, 0, 1);
base.Update();
}
@ -279,67 +320,84 @@ namespace osu.Game.Screens.Menu
state = value;
switch (state)
{
case ButtonState.Contracted:
switch (ContractStyle)
{
default:
box.ScaleTo(new Vector2(0, 1), 500, Easing.OutExpo);
this.FadeOut(500);
break;
case 1:
box.ScaleTo(new Vector2(0, 1), 400, Easing.InSine);
this.FadeOut(800);
break;
}
break;
case ButtonState.Expanded:
const int expand_duration = 500;
box.ScaleTo(new Vector2(1, 1), expand_duration, Easing.OutExpo);
this.FadeIn(expand_duration / 6f);
break;
case ButtonState.Exploded:
const int explode_duration = 200;
box.ScaleTo(new Vector2(2, 1), explode_duration, Easing.OutExpo);
this.FadeOut(explode_duration / 4f * 3);
break;
}
animateState();
StateChanged?.Invoke(State);
}
}
private void animateState()
{
switch (state)
{
case ButtonState.Contracted:
switch (ContractStyle)
{
default:
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 500, Easing.OutExpo);
this.FadeOut(500);
break;
case 1:
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(0, 1)), 400, Easing.InSine);
this.FadeOut(800);
break;
}
break;
case ButtonState.Expanded:
const int expand_duration = 500;
background.ResizeTo(initialSize, expand_duration, Easing.OutExpo);
this.FadeIn(expand_duration / 6f);
break;
case ButtonState.Exploded:
const int explode_duration = 200;
background.ResizeTo(Vector2.Multiply(initialSize, new Vector2(2, 1)), explode_duration, Easing.OutExpo);
this.FadeOut(explode_duration / 4f * 3);
break;
}
}
private ButtonSystemState buttonSystemState;
public ButtonSystemState ButtonSystemState
{
get => buttonSystemState;
set
{
ContractStyle = 0;
if (buttonSystemState == value)
return;
switch (value)
{
case ButtonSystemState.Initial:
buttonSystemState = value;
UpdateState();
}
}
protected virtual void UpdateState()
{
ContractStyle = 0;
switch (ButtonSystemState)
{
case ButtonSystemState.Initial:
State = ButtonState.Contracted;
break;
case ButtonSystemState.EnteringMode:
ContractStyle = 1;
State = ButtonState.Contracted;
break;
default:
if (ButtonSystemState <= VisibleStateMax && ButtonSystemState >= VisibleStateMin)
State = ButtonState.Expanded;
else if (ButtonSystemState < VisibleStateMin)
State = ButtonState.Contracted;
break;
case ButtonSystemState.EnteringMode:
ContractStyle = 1;
State = ButtonState.Contracted;
break;
default:
if (value <= VisibleStateMax && value >= VisibleStateMin)
State = ButtonState.Expanded;
else if (value < VisibleStateMin)
State = ButtonState.Contracted;
else
State = ButtonState.Exploded;
break;
}
else
State = ButtonState.Exploded;
break;
}
}
}

View File

@ -248,21 +248,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(passwordTextBox));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(passwordTextBox));
passwordTextBox.OnCommit += (_, _) => performJoin();
}
private void performJoin()
{
lounge?.Join(room, passwordTextBox.Text, null, joinFailed);
GetContainingInputManager().TriggerFocusContention(passwordTextBox);
GetContainingFocusManager().TriggerFocusContention(passwordTextBox);
}
private void joinFailed(string error) => Schedule(() =>
{
passwordTextBox.Text = string.Empty;
GetContainingInputManager().ChangeFocus(passwordTextBox);
GetContainingFocusManager().ChangeFocus(passwordTextBox);
errorText.Text = error;
errorText

View File

@ -1,6 +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.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Lounge;
namespace osu.Game.Screens.OnlinePlay.Playlists
@ -10,5 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
protected override string ScreenTitle => "Playlists";
protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen();
public void Join(Room room) => Schedule(() => Lounge.Join(room, string.Empty));
}
}

View File

@ -245,7 +245,7 @@ namespace osu.Game.Screens.Select
searchTextBox.ReadOnly = true;
searchTextBox.HoldFocus = false;
if (searchTextBox.HasFocus)
GetContainingInputManager().ChangeFocus(searchTextBox);
GetContainingFocusManager().ChangeFocus(searchTextBox);
}
public void Activate()

View File

@ -20,24 +20,25 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Screens.Select;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using WebCommonStrings = osu.Game.Resources.Localisation.Web.CommonStrings;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.SelectV2.Footer
{
public partial class BeatmapOptionsPopover : OsuPopover
{
private FillFlowContainer buttonFlow = null!;
private readonly FooterButtonOptionsV2 footerButton;
private readonly ScreenFooterButtonOptions footerButton;
private WorkingBeatmap beatmapWhenOpening = null!;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
public BeatmapOptionsPopover(FooterButtonOptionsV2 footerButton)
public BeatmapOptionsPopover(ScreenFooterButtonOptions footerButton)
{
this.footerButton = footerButton;
}
@ -77,7 +78,7 @@ namespace osu.Game.Screens.Select.FooterV2
{
base.LoadComplete();
ScheduleAfterChildren(() => GetContainingInputManager().ChangeFocus(this));
ScheduleAfterChildren(() => GetContainingFocusManager().ChangeFocus(this));
beatmap.BindValueChanged(_ => Hide());
}

View File

@ -21,14 +21,15 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Play.HUD;
using osu.Game.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.SelectV2.Footer
{
public partial class FooterButtonModsV2 : FooterButtonV2, IHasCurrentValue<IReadOnlyList<Mod>>
public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
{
// todo: see https://github.com/ppy/osu-framework/issues/3271
private const float torus_scale_factor = 1.2f;

View File

@ -8,10 +8,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Footer;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.SelectV2.Footer
{
public partial class FooterButtonOptionsV2 : FooterButtonV2, IHasPopover
public partial class ScreenFooterButtonOptions : ScreenFooterButton, IHasPopover
{
[BackgroundDependencyLoader]
private void load(OsuColour colour)

View File

@ -10,12 +10,13 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Footer;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Select.FooterV2
namespace osu.Game.Screens.SelectV2.Footer
{
public partial class FooterButtonRandomV2 : FooterButtonV2
public partial class ScreenFooterButtonRandom : ScreenFooterButton
{
public Action? NextRandom { get; set; }
public Action? PreviousRandom { get; set; }

View File

@ -21,6 +21,9 @@ namespace osu.Game.Tests.Visual.Metadata
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
@ -77,5 +80,11 @@ namespace osu.Game.Tests.Visual.Metadata
=> Task.FromResult(new BeatmapUpdates(Array.Empty<int>(), queueId));
public override Task BeatmapSetsUpdated(BeatmapUpdates updates) => Task.CompletedTask;
public override Task DailyChallengeUpdated(DailyChallengeInfo? info)
{
dailyChallengeInfo.Value = info;
return Task.CompletedTask;
}
}
}

View File

@ -30,13 +30,13 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2023.1117.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2024.517.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.510.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.523.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.517.0" />
<PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.36.0" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.509.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.523.0" />
</ItemGroup>
</Project>