1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-14 22:43:05 +08:00

Merge branch 'master' into fix-op-non-current-onexiting

This commit is contained in:
Dean Herbert 2023-01-09 19:12:42 +09:00 committed by GitHub
commit 54b2fe9df3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
196 changed files with 2343 additions and 824 deletions

View File

@ -1,6 +1,6 @@
clone_depth: 1
version: '{build}'
image: Visual Studio 2019
image: Visual Studio 2022
test: off
skip_non_tags: true
configuration: Release
@ -83,4 +83,4 @@ artifacts:
deploy:
- provider: Environment
name: nuget
name: nuget

View File

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

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" package="sh.ppy.osulazer" android:installLocation="auto" android:versionName="0.1.0">
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="sh.ppy.osulazer" android:installLocation="auto">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<application android:allowBackup="true" android:supportsRtl="true" android:label="osu!" android:icon="@drawable/lazer" />
</manifest>

View File

@ -8,6 +8,9 @@
<UseMauiEssentials>true</UseMauiEssentials>
<!-- This currently causes random lockups during gameplay. https://github.com/mono/mono/issues/18973 -->
<EnableLLVM>false</EnableLLVM>
<Version>0.0.0</Version>
<ApplicationVersion Condition=" '$(ApplicationVersion)' == '' ">1</ApplicationVersion>
<ApplicationDisplayVersion Condition=" '$(ApplicationDisplayVersion)' == '' ">$(Version)</ApplicationDisplayVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />

View File

@ -18,6 +18,36 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
{
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
[Test]
public void TestAlwaysHidden()
{
CreateModTest(new ModTestData
{
Mod = new CatchModNoScope
{
HiddenComboCount = { Value = 0 },
},
Autoplay = true,
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Fruit
{
X = CatchPlayfield.CENTER_X * 0.5f,
StartTime = 1000,
},
new Fruit
{
X = CatchPlayfield.CENTER_X * 1.5f,
StartTime = 2000,
}
}
}
});
}
[Test]
public void TestVisibleDuringBreak()
{

View File

@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.Mods
var catchPlayfield = (CatchPlayfield)playfield;
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
// AlwaysPresent required for catcher to still act on input when fully hidden.
catchPlayfield.CatcherArea.AlwaysPresent = true;
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -32,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
foreach (var state in Enum.GetValues(typeof(CatcherAnimationState)).Cast<CatcherAnimationState>())
foreach (var state in Enum.GetValues<CatcherAnimationState>())
{
AddInternal(drawables[state] = getDrawableFor(state).With(d =>
{

View File

@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.UI
{
@ -106,41 +105,17 @@ namespace osu.Game.Rulesets.Catch.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
}
protected override bool OnTouchDown(TouchDownEvent e)
{
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
Show();
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
// multiple mouse buttons may be pressed and handling the same action.
foreach (MouseButton button in e.PressedButtons)
updateAction(button, action);
return false;
}
protected override void OnTouchMove(TouchMoveEvent e)
{
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
base.OnTouchMove(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
updateAction(e.Button, null);
base.OnMouseUp(e);
}
protected override void OnTouchUp(TouchUpEvent e)
{
updateAction(e.Touch.Source, null);

View File

@ -209,18 +209,6 @@ namespace osu.Game.Rulesets.Mania.UI
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
keyBindingContainer?.TriggerReleased(column.Action.Value);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
keyBindingContainer?.TriggerPressed(column.Action.Value);

View File

@ -160,9 +160,9 @@ namespace osu.Game.Rulesets.Osu.Tests
static bool assertSamples(HitObject hitObject) => hitObject.Samples.All(s => s.Name != HitSampleInfo.HIT_CLAP && s.Name != HitSampleInfo.HIT_WHISTLE);
}
private Drawable testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats);
private Drawable testSimpleBig(int repeats = 0) => createSlider(repeats: repeats);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10);
private Drawable testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(repeats: repeats, stackHeight: 10);
private Drawable testDistanceOverflow(int repeats = 0)
{

View File

@ -1,35 +1,64 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonFollowCircle : FollowCircle
{
private readonly CircularContainer circleContainer;
private readonly Box circleFill;
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
public ArgonFollowCircle()
{
InternalChild = new CircularContainer
InternalChild = circleContainer = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderThickness = 4,
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
Blending = BlendingParameters.Additive,
Child = new Box
Child = circleFill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Alpha = 0.3f,
}
};
}
[BackgroundDependencyLoader]
private void load()
{
if (parentObject != null)
accentColour.BindTo(parentObject.AccentColour);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
circleContainer.BorderColour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
circleFill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
}, true);
}
protected override void OnSliderPress()
{
const float duration = 300f;

View File

@ -2,6 +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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
@ -37,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
fill = new Box
{
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -53,10 +56,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
};
}
[BackgroundDependencyLoader]
private void load()
{
if (parentObject != null)
accentColour.BindTo(parentObject.AccentColour);
}
protected override void LoadComplete()
{
base.LoadComplete();
accentColour.BindValueChanged(colour =>
{
fill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
}, true);
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
foreach (var result in Enum.GetValues<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -107,24 +107,6 @@ namespace osu.Game.Rulesets.Taiko.UI
return false;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!validMouse(e))
return false;
handleDown(e.Button, e.ScreenSpaceMousePosition);
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!validMouse(e))
return;
handleUp(e.Button);
base.OnMouseUp(e);
}
protected override bool OnTouchDown(TouchDownEvent e)
{
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);

View File

@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
foreach (var result in Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
explosionPools.Add(result, new HitExplosionPool(result));

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldAspect = new BindableBool(true);
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true);
protected override void Update()
{
@ -21,7 +21,12 @@ namespace osu.Game.Rulesets.Taiko.UI
float height = default_relative_height;
if (LockPlayfieldAspect.Value)
// Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
// We originally wanted to limit this more, but there was considerable pushback from the community.
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;

View File

@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Judgements;
@ -139,6 +140,29 @@ namespace osu.Game.Tests.Gameplay
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
}
[Test]
public void TestAccuracyModes()
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList<HitObject>()
};
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }

View File

@ -60,6 +60,6 @@ namespace osu.Game.Tests.Mods
/// This local helper is used rather than <see cref="Ruleset.CreateAllMods"/>, because the aforementioned method flattens multi mods.
/// </remarks>>
private static IEnumerable<MultiMod> getMultiMods(Ruleset ruleset)
=> Enum.GetValues(typeof(ModType)).Cast<ModType>().SelectMany(ruleset.GetModsFor).OfType<MultiMod>();
=> Enum.GetValues<ModType>().SelectMany(ruleset.GetModsFor).OfType<MultiMod>();
}
}

View File

@ -6,6 +6,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Timelines.Summary;
@ -21,7 +22,13 @@ namespace osu.Game.Tests.Visual.Editing
public TestSceneEditorSummaryTimeline()
{
editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
editorBeatmap = new EditorBeatmap(beatmap);
}
protected override void LoadComplete()

View File

@ -0,0 +1,35 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
namespace osu.Game.Tests.Visual.Editing
{
public partial class TestScenePreviewTime : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestSceneSetPreviewTimingPoint()
{
AddStep("seek to 1000", () => EditorClock.Seek(1000));
AddAssert("time is 1000", () => EditorClock.CurrentTime == 1000);
AddStep("set current time as preview point", () => Editor.SetPreviewPointToCurrentTime());
AddAssert("preview time is 1000", () => EditorBeatmap.PreviewTime.Value == 1000);
}
[Test]
public void TestScenePreviewTimeline()
{
AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any());
AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha == 1);
}
}
}

View File

@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
prepareTestAPI(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset
createPlayerTest(createRuleset: () => new OsuRuleset
{
RulesetInfo =
{

View File

@ -201,6 +201,23 @@ namespace osu.Game.Tests.Visual.Menus
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
}
[Test]
public void TestRulesetSelectorOverflow()
{
AddStep("set toolbar width", () =>
{
toolbar.RelativeSizeAxes = Axes.None;
toolbar.Width = 400;
});
AddStep("move mouse over news toggle button", () =>
{
var button = toolbar.ChildrenOfType<ToolbarNewsButton>().Single();
InputManager.MoveMouseTo(button);
});
AddAssert("no ruleset toggle buttons hovered", () => !toolbar.ChildrenOfType<ToolbarRulesetTabButton>().Any(button => button.IsHovered));
AddUntilStep("toolbar gradient visible", () => toolbar.ChildrenOfType<Toolbar.ToolbarBackground>().Single().Children.All(d => d.Alpha > 0));
}
public partial class TestToolbar : Toolbar
{
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;

View File

@ -3,6 +3,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -46,10 +47,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
[Test]
public void TestNextItemSelectedAfterDeletion()
[TestCase(true)]
[TestCase(false)]
public void TestNextItemSelectedAfterDeletion(bool allowSelection)
{
createPlaylist();
createPlaylist(p =>
{
p.AllowSelection = allowSelection;
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@ -57,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
AddAssert("item 0 is " + (allowSelection ? "selected" : "not selected"), () => playlist.SelectedItem.Value == (allowSelection ? playlist.Items[0] : null));
}
[Test]
@ -117,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
});
private void createPlaylist()
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{
AddStep("create playlist", () =>
{
@ -154,6 +159,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));

View File

@ -54,6 +54,8 @@ namespace osu.Game.Tests.Visual.Online
{
overlay.ShowBeatmapSet(new APIBeatmapSet
{
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Future genre" },
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "Future language" },
OnlineID = 1235,
Title = @"an awesome beatmap",
Artist = @"naru narusegawa",

View File

@ -530,6 +530,52 @@ namespace osu.Game.Tests.Visual.Online
});
}
[Test]
public void TestTextBoxSavePerChannel()
{
var testPMChannel = new Channel(testUser);
AddStep("show overlay", () => chatOverlay.Show());
joinTestChannel(0);
joinChannel(testPMChannel);
AddAssert("listing is visible", () => listingIsVisible);
AddStep("search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
AddAssert("'number 2' saved to selector", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "number 2");
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("text box cleared on normal channel", () => chatOverlayTextBox.Text == string.Empty);
AddAssert("nothing saved on normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
AddStep("type '727'", () => chatOverlayTextBox.Text = "727");
AddAssert("'727' saved to normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "727");
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddAssert("text box cleared on PM channel", () => chatOverlayTextBox.Text == string.Empty);
AddAssert("nothing saved on PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
AddStep("type 'hello'", () => chatOverlayTextBox.Text = "hello");
AddAssert("'hello' saved to PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "hello");
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
AddAssert("text box contains '727'", () => chatOverlayTextBox.Text == "727");
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
AddAssert("text box contains 'hello'", () => chatOverlayTextBox.Text == "hello");
AddStep("click close button", () =>
{
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
clickDrawable(closeButton);
});
AddAssert("listing is visible", () => listingIsVisible);
AddAssert("text box contains 'channel 2'", () => chatOverlayTextBox.Text == "number 2");
AddUntilStep("only channel 2 visible", () =>
{
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
.Where(item => item.IsPresent);
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
});
}
private void joinTestChannel(int i)
{
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));

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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

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 osu.Game.Overlays.Profile.Sections.Kudosu;
using System.Collections.Generic;
using System;

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 osu.Framework.Graphics;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Catch;
@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
ProfileRulesetSelector selector;
var user = new Bindable<APIUser>();
var user = new Bindable<APIUser?>();
Child = selector = new ProfileRulesetSelector
{

View File

@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Online
private Action<GetUsersRequest>? handleGetUsersRequest;
private Action<GetUserRequest>? handleGetUserRequest;
private IDisposable? subscription;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
@ -246,6 +248,26 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
}
[Test]
public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
{
int userId = getUserId();
setUpUser(userId);
long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("unsubscribe", () => subscription!.Dispose());
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddWaitStep("wait a bit", 5);
AddAssert("update not received", () => update == null);
}
private int nextUserId = 2000;
private long nextScoreId = 50000;
@ -266,7 +288,7 @@ namespace osu.Game.Tests.Visual.Online
}
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => watcher.RegisterForStatisticsUpdateAfter(
AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
{
Ruleset = rulesetInfo,

View File

@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online
private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
private TestStandAloneChatDisplay chatWithTextBox;
private TestStandAloneChatDisplay chatWithTextBox2;
private int messageIdSequence;
private Channel testChannel;
@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Online
private void reinitialiseDrawableDisplay()
{
Children = new[]
Children = new Drawable[]
{
chatDisplay = new TestStandAloneChatDisplay
{
@ -88,13 +90,28 @@ namespace osu.Game.Tests.Visual.Online
Size = new Vector2(400, 80),
Channel = { Value = testChannel },
},
new TestStandAloneChatDisplay(true)
new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
Children = new[]
{
chatWithTextBox = new TestStandAloneChatDisplay(true)
{
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
},
chatWithTextBox2 = new TestStandAloneChatDisplay(true)
{
Margin = new MarginPadding(20),
Size = new Vector2(400, 150),
Channel = { Value = testChannel },
},
}
}
};
}
@ -351,6 +368,13 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
[Test]
public void TestTextBoxSync()
{
AddStep("type 'hello' to text box 1", () => chatWithTextBox.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text = "hello");
AddAssert("text box 2 contains 'hello'", () => chatWithTextBox2.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text == "hello");
}
private void fillChat(int count = 10)
{
AddStep("fill chat", () =>

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 osu.Framework.Allocation;
using osu.Framework.Graphics;

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;
@ -20,7 +18,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
private ProfileHeader header;
private ProfileHeader header = null!;
[SetUpSteps]
public void SetUpSteps()

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;

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 NUnit.Framework;
using osu.Framework.Graphics;
@ -14,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
{
private PreviousUsernames container;
private PreviousUsernames container = null!;
[SetUp]
public void SetUp() => Schedule(() =>
@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("Is hidden", () => container.Alpha == 0);
}
private static readonly APIUser[] users =
private static readonly APIUser?[] users =
{
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },

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.Collections.Generic;
using System.Linq;
using NUnit.Framework;

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 osu.Framework.Allocation;
using osu.Framework.Graphics;

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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -0,0 +1,117 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Online.Solo;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics.User;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneOverallRanking : OsuTestScene
{
private OverallRanking overallRanking = null!;
[Test]
public void TestUpdatePending()
{
createDisplay();
}
[Test]
public void TestAllIncreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
},
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 99.07,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
});
}
[Test]
public void TestAllDecreased()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 1_234,
Accuracy = 99.07,
MaxCombo = 2_352,
RankedScore = 23_124_231_435,
TotalScore = 123_124_231_435,
PP = 5_434
},
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
});
}
[Test]
public void TestNoChanges()
{
var statistics = new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
};
createDisplay();
displayUpdate(statistics, statistics);
}
[Test]
public void TestNotRanked()
{
var statistics = new UserStatistics
{
GlobalRank = null,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = null
};
createDisplay();
displayUpdate(statistics, statistics);
}
private void createDisplay() => AddStep("create display", () => Child = overallRanking = new OverallRanking
{
Width = 400,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new SoloStatisticsUpdate(new ScoreInfo(), before, after));
}
}

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 NUnit.Framework;
using osu.Game.Overlays.Profile.Sections;
using osu.Framework.Testing;
@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private ProfileSubsectionHeader header;
private ProfileSubsectionHeader header = null!;
[Test]
public void TestHiddenCounter()

View File

@ -55,6 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface
};
});
[Test]
public void TestDisplay()
{
AddRepeatStep("toggle expanded state", () =>
{
InputManager.MoveMouseTo(group.ChildrenOfType<IconButton>().Single());
InputManager.Click(MouseButton.Left);
}, 5);
}
[Test]
public void TestClickExpandButtonMultipleTimes()
{

View File

@ -127,7 +127,7 @@ namespace osu.Game.Tournament.IPC
using (var stream = IPCStorage.GetStream(file_ipc_state_filename))
using (var sr = new StreamReader(stream))
{
State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine().AsNonNull());
State.Value = Enum.Parse<TourneyState>(sr.ReadLine().AsNonNull());
}
}
catch (Exception)

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
continue;
// ReSharper disable once PossibleNullReferenceException
string[] split = line.Split(':');
string[] split = line.Split(':', StringSplitOptions.TrimEntries);
if (split.Length < 2)
{
@ -55,9 +55,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components
teams.Add(new TournamentTeam
{
FullName = { Value = split[1].Trim(), },
Acronym = { Value = split.Length >= 3 ? split[2].Trim() : null, },
FlagName = { Value = split[0].Trim() }
FullName = { Value = split[1], },
Acronym = { Value = split.Length >= 3 ? split[2] : null, },
FlagName = { Value = split[0] }
});
}
}

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Editors
{
var countries = new List<TournamentTeam>();
foreach (var country in Enum.GetValues(typeof(CountryCode)).Cast<CountryCode>().Skip(1))
foreach (var country in Enum.GetValues<CountryCode>().Skip(1))
{
countries.Add(new TournamentTeam
{

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
public struct BeatmapSetOnlineNomination
{
[JsonProperty(@"beatmapset_id")]
public int BeatmapsetId { get; set; }
[JsonProperty(@"reset")]
public bool Reset { get; set; }
[JsonProperty(@"rulesets")]
public string[]? Rulesets { get; set; }
[JsonProperty(@"user_id")]
public int UserId { get; set; }
}
}

View File

@ -5,16 +5,19 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Localisation;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public abstract partial class BeatmapCard : OsuClickableContainer
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
@ -96,5 +99,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
};
}
}

View File

@ -138,9 +138,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
if (Expanded.Value)
{
background.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
else
{
background.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
dropdownContent.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
borderContainer.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
}
content.TweenEdgeEffectTo(new EdgeEffectParameters
{

View File

@ -160,7 +160,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"SampleSet":
defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value);
defaultSampleBank = Enum.Parse<LegacySampleBank>(pair.Value);
break;
case @"SampleVolume":
@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break;
case @"Countdown":
beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value);
beatmap.BeatmapInfo.Countdown = Enum.Parse<CountdownType>(pair.Value);
break;
case @"CountdownOffset":

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps.Formats
{
var comboColour = colours[i];
writer.Write(FormattableString.Invariant($"Combo{i}: "));
writer.Write(FormattableString.Invariant($"Combo{1 + i}: "));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));

View File

@ -132,13 +132,7 @@ namespace osu.Game.Beatmaps.Formats
protected KeyValuePair<string, string> SplitKeyVal(string line, char separator = ':', bool shouldTrim = true)
{
string[] split = line.Split(separator, 2);
if (shouldTrim)
{
for (int i = 0; i < split.Length; i++)
split[i] = split[i].Trim();
}
string[] split = line.Split(separator, 2, shouldTrim ? StringSplitOptions.TrimEntries : StringSplitOptions.None);
return new KeyValuePair<string, string>
(

View File

@ -301,11 +301,11 @@ namespace osu.Game.Beatmaps.Formats
}
}
private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString();
private string parseLayer(string value) => Enum.Parse<LegacyStoryLayer>(value).ToString();
private Anchor parseOrigin(string value)
{
var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value);
var origin = Enum.Parse<LegacyOrigins>(value);
switch (origin)
{
@ -343,8 +343,8 @@ namespace osu.Game.Beatmaps.Formats
private AnimationLoopType parseAnimationLoopType(string value)
{
var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value);
return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever;
var parsed = Enum.Parse<AnimationLoopType>(value);
return Enum.IsDefined(parsed) ? parsed : AnimationLoopType.LoopForever;
}
private void handleVariables(string line)

View File

@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
public Waveform Waveform => waveform.Value;
public Storyboard Storyboard => storyboard.Value;
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
@ -48,10 +46,11 @@ namespace osu.Game.Beatmaps
private readonly object beatmapFetchLock = new object();
private readonly Lazy<Waveform> waveform;
private readonly Lazy<Storyboard> storyboard;
private readonly Lazy<ISkin> skin;
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
private Waveform waveform; // waveform is also not Lazy as the track may change.
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
{
@ -60,7 +59,6 @@ namespace osu.Game.Beatmaps
BeatmapInfo = beatmapInfo;
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
waveform = new Lazy<Waveform>(GetWaveform);
storyboard = new Lazy<Storyboard>(GetStoryboard);
skin = new Lazy<ISkin>(GetSkin);
}
@ -108,7 +106,16 @@ namespace osu.Game.Beatmaps
public virtual bool TrackLoaded => track != null;
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
public Track LoadTrack()
{
track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
// the track may have changed, recycle the current waveform.
waveform?.Dispose();
waveform = null;
return track;
}
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
{
@ -171,6 +178,12 @@ namespace osu.Game.Beatmaps
#endregion
#region Waveform
public Waveform Waveform => waveform ??= GetWaveform();
#endregion
#region Beatmap
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;

View File

@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Linq;
using osu.Framework;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
@ -63,10 +62,10 @@ namespace osu.Game.Database
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows && data is FileStream fs && preferHardLinks)
if (data is FileStream fs && preferHardLinks)
{
// attempt to do a fast hard link rather than copy.
if (HardLinkHelper.CreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name, IntPtr.Zero))
if (HardLinkHelper.TryCreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name))
return;
}

View File

@ -22,38 +22,8 @@ namespace osu.Game.Graphics
public static Color4 Gray(byte amt) => new Color4(amt, amt, amt, 255);
/// <summary>
/// Retrieves the colour for a <see cref="DifficultyRating"/>.
/// Retrieves the colour for a given point in the star range.
/// </summary>
/// <remarks>
/// Sourced from the @diff-{rating} variables in https://github.com/ppy/osu-web/blob/71fbab8936d79a7929d13854f5e854b4f383b236/resources/assets/less/variables.less.
/// </remarks>
public Color4 ForDifficultyRating(DifficultyRating difficulty, bool useLighterColour = false)
{
switch (difficulty)
{
case DifficultyRating.Easy:
return Color4Extensions.FromHex("4ebfff");
case DifficultyRating.Normal:
return Color4Extensions.FromHex("66ff91");
case DifficultyRating.Hard:
return Color4Extensions.FromHex("f7e85d");
case DifficultyRating.Insane:
return Color4Extensions.FromHex("ff7e68");
case DifficultyRating.Expert:
return Color4Extensions.FromHex("fe3c71");
case DifficultyRating.ExpertPlus:
return Color4Extensions.FromHex("6662dd");
default:
throw new ArgumentOutOfRangeException(nameof(difficulty));
}
}
public Color4 ForStarDifficulty(double starDifficulty) => ColourUtils.SampleFromLinearGradient(new[]
{
(0.1f, Color4Extensions.FromHex("aaaaaa")),

View File

@ -82,7 +82,7 @@ namespace osu.Game.Graphics.UserInterface
if (Link != null)
{
items.Add(new OsuMenuItem("Open", MenuItemType.Standard, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Open", MenuItemType.Highlighted, () => host.OpenUrlExternally(Link)));
items.Add(new OsuMenuItem("Copy URL", MenuItemType.Standard, copyUrl));
}

View File

@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
return false;
switch (e.Key)
{
case Key.Up:

View File

@ -12,7 +12,7 @@ namespace osu.Game.Graphics.UserInterface
{
public OsuEnumDropdown()
{
Items = (T[])Enum.GetValues(typeof(T));
Items = Enum.GetValues<T>();
}
}
}

View File

@ -14,9 +14,8 @@ namespace osu.Game.IO
{
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
{
// We can support other operating systems quite easily in the future.
// Let's handle the most common one for now, though.
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
// For simplicity, only support desktop operating systems for now.
if (!RuntimeInfo.IsDesktop)
return false;
const string test_filename = "_hard_link_test";
@ -31,7 +30,7 @@ namespace osu.Game.IO
File.WriteAllText(testSourcePath, string.Empty);
// Test availability by creating an arbitrary hard link between the source and destination paths.
return CreateHardLink(testDestinationPath, testSourcePath, IntPtr.Zero);
return TryCreateHardLink(testDestinationPath, testSourcePath);
}
catch
{
@ -55,21 +54,60 @@ namespace osu.Game.IO
}
}
/// <summary>
/// Attempts to create a hard link from <paramref name="sourcePath"/> to <paramref name="destinationPath"/>,
/// using platform-specific native methods.
/// </summary>
/// <remarks>
/// Hard links are only available on desktop platforms.
/// </remarks>
/// <returns>Whether the hard link was successfully created.</returns>
public static bool TryCreateHardLink(string destinationPath, string sourcePath)
{
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero);
case RuntimeInfo.Platform.Linux:
case RuntimeInfo.Platform.macOS:
return link(sourcePath, destinationPath) == 0;
default:
return false;
}
}
// For future use (to detect if a file is a hard link with other references existing on disk).
public static int GetFileLinkCount(string filePath)
{
int result = 0;
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
ByHandleFileInformation fileInfo;
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
ByHandleFileInformation fileInfo;
if (GetFileInformationByHandle(handle, out fileInfo))
result = (int)fileInfo.NumberOfLinks;
CloseHandle(handle);
break;
case RuntimeInfo.Platform.Linux:
case RuntimeInfo.Platform.macOS:
if (stat(filePath, out var statbuf) == 0)
result = (int)statbuf.st_nlink;
break;
}
return result;
}
#region Windows native methods
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
@ -104,5 +142,49 @@ namespace osu.Game.IO
public readonly uint FileIndexHigh;
public readonly uint FileIndexLow;
}
#endregion
#region Linux native methods
#pragma warning disable IDE1006 // Naming rule violation
[DllImport("libc", SetLastError = true)]
public static extern int link(string oldpath, string newpath);
[DllImport("libc", SetLastError = true)]
private static extern int stat(string pathname, out struct_stat statbuf);
// ReSharper disable once InconsistentNaming
// Struct layout is likely non-portable across unices. Tread with caution.
[StructLayout(LayoutKind.Sequential)]
private struct struct_stat
{
public readonly long st_dev;
public readonly long st_ino;
public readonly long st_nlink;
public readonly int st_mode;
public readonly int st_uid;
public readonly int st_gid;
public readonly long st_rdev;
public readonly long st_size;
public readonly long st_blksize;
public readonly long st_blocks;
public readonly timespec st_atim;
public readonly timespec st_mtim;
public readonly timespec st_ctim;
}
// ReSharper disable once InconsistentNaming
[StructLayout(LayoutKind.Sequential)]
private struct timespec
{
public readonly long tv_sec;
public readonly long tv_nsec;
}
#pragma warning restore IDE1006
#endregion
}
}

View File

@ -0,0 +1,24 @@
// 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;
namespace osu.Game.Localisation
{
public static class ContextMenuStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.ContextMenu";
/// <summary>
/// "View profile"
/// </summary>
public static LocalisableString ViewProfile => new TranslatableString(getKey(@"view_profile"), @"View profile");
/// <summary>
/// "View beatmap"
/// </summary>
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -259,7 +259,11 @@ namespace osu.Game.Online.API
var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => state.Value = APIState.Failing;
friendsReq.Success += res => friends.AddRange(res);
friendsReq.Success += res =>
{
friends.Clear();
friends.AddRange(res);
};
if (!handleRequest(friendsReq))
{

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 osu.Game.Beatmaps;
using osu.Game.Rulesets;
@ -11,10 +9,11 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using System.Text;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<APIScoresCollection>
public class GetScoresRequest : APIRequest<APIScoresCollection>, IEquatable<GetScoresRequest>
{
public const int MAX_SCORES_PER_REQUEST = 50;
@ -23,7 +22,7 @@ namespace osu.Game.Online.API.Requests
private readonly IRulesetInfo ruleset;
private readonly IEnumerable<IMod> mods;
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod> mods = null)
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod>? mods = null)
{
if (beatmapInfo.OnlineID <= 0)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
@ -51,5 +50,16 @@ namespace osu.Game.Online.API.Requests
return query.ToString();
}
public bool Equals(GetScoresRequest? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return beatmapInfo.Equals(other.beatmapInfo)
&& scope == other.scope
&& ruleset.Equals(other.ruleset)
&& mods.SequenceEqual(other.mods);
}
}
}

View File

@ -111,6 +111,12 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"language")]
public BeatmapSetOnlineLanguage Language { get; set; }
[JsonProperty(@"current_nominations")]
public BeatmapSetOnlineNomination[]? CurrentNominations { get; set; }
[JsonProperty(@"related_users")]
public APIUser[]? RelatedUsers { get; set; }
public string Source { get; set; } = string.Empty;
[JsonProperty(@"tags")]

View File

@ -21,7 +21,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string type
{
set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.ToPascalCase());
set => Type = Enum.Parse<RecentActivityType>(value.ToPascalCase());
}
public RecentActivityType Type;
@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty]
private string scoreRank
{
set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value);
set => ScoreRank = Enum.Parse<ScoreRank>(value);
}
public ScoreRank ScoreRank;

View File

@ -185,7 +185,7 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"playstyle")]
private string[] playStyle
{
set => PlayStyles = value?.Select(str => Enum.Parse(typeof(APIPlayStyle), str, true)).Cast<APIPlayStyle>().ToArray();
set => PlayStyles = value?.Select(str => Enum.Parse<APIPlayStyle>(str, true)).ToArray();
}
public APIPlayStyle[] PlayStyles;

View File

@ -98,6 +98,11 @@ namespace osu.Game.Online.Chat
/// </summary>
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
/// <summary>
/// The current text box message while in this <see cref="Channel"/>.
/// </summary>
public Bindable<string> TextBoxMessage = new Bindable<string>(string.Empty);
[JsonConstructor]
public Channel()
{

View File

@ -341,6 +341,8 @@ namespace osu.Game.Online.Chat
OpenWiki,
Custom,
OpenChangelog,
FilterBeatmapSetGenre,
FilterBeatmapSetLanguage,
}
public class Link : IComparable<Link>

View File

@ -111,8 +111,13 @@ namespace osu.Game.Online.Chat
{
drawableChannel?.Expire();
if (e.OldValue != null)
TextBox?.Current.UnbindFrom(e.OldValue.TextBoxMessage);
if (e.NewValue == null) return;
TextBox?.Current.BindTo(e.NewValue.TextBoxMessage);
drawableChannel = CreateDrawableChannel(e.NewValue);
drawableChannel.CreateChatLineAction = CreateMessage;
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };

View File

@ -9,7 +9,8 @@ namespace osu.Game.Online
{
public ProductionEndpointConfiguration()
{
WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh";
WebsiteRootUrl = @"https://osu.ppy.sh";
APIEndpointUrl = @"https://lazer.ppy.sh";
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";

View File

@ -46,24 +46,30 @@ namespace osu.Game.Online.Solo
/// </summary>
/// <param name="score">The score to listen for the statistics update for.</param>
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
public void RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady) => Schedule(() =>
/// <returns>An <see cref="IDisposable"/> representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.</returns>
public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
{
if (!api.IsLoggedIn)
return;
if (!score.Ruleset.IsLegacyRuleset())
return;
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
if (lastProcessedScoreId == score.OnlineID)
Schedule(() =>
{
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
return;
}
if (!api.IsLoggedIn)
return;
callbacks.Add(score.OnlineID, callback);
});
if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
return;
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
if (lastProcessedScoreId == score.OnlineID)
{
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
return;
}
callbacks.Add(score.OnlineID, callback);
});
return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
}
private void onUserChanged(APIUser? localUser) => Schedule(() =>
{
@ -75,15 +81,27 @@ namespace osu.Game.Online.Solo
return;
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
userRequest.Success += response => Schedule(() =>
{
latestStatistics = new Dictionary<string, UserStatistics>();
foreach (var rulesetStats in response.Users.Single().RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
});
userRequest.Success += initialiseUserStatistics;
api.Queue(userRequest);
});
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
{
var user = response.Users.SingleOrDefault();
// possible if the user is restricted or similar.
if (user == null)
return;
latestStatistics = new Dictionary<string, UserStatistics>();
if (user.RulesetsStatistics != null)
{
foreach (var rulesetStats in user.RulesetsStatistics)
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
}
});
private void userScoreProcessed(int userId, long scoreId)
{
if (userId != api.LocalUser.Value?.OnlineID)

View File

@ -46,6 +46,7 @@ using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.Music;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Toolbar;
@ -353,7 +354,18 @@ namespace osu.Game
break;
case LinkAction.SearchBeatmapSet:
SearchBeatmapSet(argString);
if (link.Argument is RomanisableString romanisable)
SearchBeatmapSet(romanisable.GetPreferred(Localisation.CurrentParameters.Value.PreferOriginalScript));
else
SearchBeatmapSet(argString);
break;
case LinkAction.FilterBeatmapSetGenre:
FilterBeatmapSetGenre((SearchGenre)link.Argument);
break;
case LinkAction.FilterBeatmapSetLanguage:
FilterBeatmapSetLanguage((SearchLanguage)link.Argument);
break;
case LinkAction.OpenEditorTimestamp:
@ -460,6 +472,10 @@ namespace osu.Game
/// <param name="query">The query to search for.</param>
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
public void FilterBeatmapSetGenre(SearchGenre genre) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithGenreFilter(genre));
public void FilterBeatmapSetLanguage(SearchLanguage language) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithLanguageFilter(language));
/// <summary>
/// Show a wiki's page as an overlay
/// </summary>
@ -723,7 +739,7 @@ namespace osu.Game
{
base.LoadComplete();
var languages = Enum.GetValues(typeof(Language)).OfType<Language>();
var languages = Enum.GetValues<Language>();
var mappings = languages.Select(language =>
{

View File

@ -46,6 +46,7 @@ using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Solo;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
@ -193,6 +194,7 @@ namespace osu.Game
protected MultiplayerClient MultiplayerClient { get; private set; }
private MetadataClient metadataClient;
private SoloStatisticsWatcher soloStatisticsWatcher;
private RealmAccess realm;
@ -301,6 +303,7 @@ namespace osu.Game
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
@ -346,6 +349,7 @@ namespace osu.Game
AddInternal(spectatorClient);
AddInternal(MultiplayerClient);
AddInternal(metadataClient);
AddInternal(soloStatisticsWatcher);
AddInternal(rulesetConfigCache);
@ -603,7 +607,7 @@ namespace osu.Game
try
{
foreach (ModType type in Enum.GetValues(typeof(ModType)))
foreach (ModType type in Enum.GetValues<ModType>())
{
dict[type] = instance.GetModsFor(type)
// Rulesets should never return null mods, but let's be defensive just in case.

View File

@ -145,6 +145,12 @@ namespace osu.Game.Overlays.BeatmapListing
public void Search(string query)
=> Schedule(() => searchControl.Query.Value = query);
public void FilterGenre(SearchGenre genre)
=> Schedule(() => searchControl.Genre.Value = genre);
public void FilterLanguage(SearchLanguage language)
=> Schedule(() => searchControl.Language.Value = language);
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -12,6 +12,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osuTK;
@ -100,9 +101,30 @@ namespace osu.Game.Overlays.BeatmapListing
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
{
private readonly Box selectedUnderline;
protected override bool HighlightOnHoverWhenActive => true;
public MultipleSelectionFilterTabItem(T value)
: base(value)
{
// This doesn't match any actual design, but should make it easier for the user to understand
// that filters are applied until we settle on a final design.
AddInternal(selectedUnderline = new Box
{
Depth = float.MaxValue,
RelativeSizeAxes = Axes.X,
Height = 1.5f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.CentreLeft,
});
}
protected override void UpdateState()
{
base.UpdateState();
selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint);
selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
}
protected override bool OnClick(ClickEvent e)

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapListing
public partial class FilterTabItem<T> : TabItem<T>
{
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
protected OverlayColourProvider ColourProvider { get; private set; }
private OsuSpriteText text;
@ -52,38 +52,42 @@ namespace osu.Game.Overlays.BeatmapListing
{
base.LoadComplete();
updateState();
UpdateState();
FinishTransforms(true);
}
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
updateState();
UpdateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
UpdateState();
}
protected override void OnActivated() => updateState();
protected override void OnActivated() => UpdateState();
protected override void OnDeactivated() => updateState();
protected override void OnDeactivated() => UpdateState();
/// <summary>
/// Returns the label text to be used for the supplied <paramref name="value"/>.
/// </summary>
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
private void updateState()
protected virtual bool HighlightOnHoverWhenActive => false;
protected virtual void UpdateState()
{
text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive);
text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
}
protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2;
}
}

View File

@ -110,6 +110,18 @@ namespace osu.Game.Overlays
ScrollFlow.ScrollToStart();
}
public void ShowWithGenreFilter(SearchGenre genre)
{
ShowWithSearch(string.Empty);
filterControl.FilterGenre(genre);
}
public void ShowWithLanguageFilter(SearchLanguage language)
{
ShowWithSearch(string.Empty);
filterControl.FilterLanguage(language);
}
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;

View File

@ -3,26 +3,29 @@
#nullable disable
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapSet.Buttons;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
@ -41,12 +44,10 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box coverGradient;
private readonly OsuSpriteText title, artist;
private readonly LinkFlowContainer title, artist;
private readonly AuthorInfo author;
private readonly ExplicitContentBeatmapBadge explicitContent;
private readonly SpotlightBeatmapBadge spotlight;
private readonly FeaturedArtistBeatmapBadge featuredArtist;
private ExternalLinkButton externalLink;
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
@ -65,8 +66,6 @@ namespace osu.Game.Overlays.BeatmapSet
public BeatmapSetHeaderContent()
{
ExternalLinkButton externalLink;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
@ -91,118 +90,74 @@ namespace osu.Game.Overlays.BeatmapSet
},
},
},
new OsuContextMenuContainer
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new Container
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = BeatmapSetOverlay.X_PADDING,
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
fadeContent = new FillFlowContainer
{
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = BeatmapSetOverlay.X_PADDING,
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
fadeContent = new FillFlowContainer
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
new Container
{
new Container
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Picker = new BeatmapPicker(),
},
title = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
})
{
Margin = new MarginPadding { Top = 15 },
},
artist = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
})
{
Margin = new MarginPadding { Bottom = 20 },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = author = new AuthorInfo(),
},
beatmapAvailability = new BeatmapAvailability(),
new Container
{
RelativeSizeAxes = Axes.X,
Height = buttons_height,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Picker = new BeatmapPicker(),
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Top = 15 },
Children = new Drawable[]
favouriteButton = new FavouriteButton
{
title = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true)
},
externalLink = new ExternalLinkButton
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font
},
explicitContent = new ExplicitContentBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10, Bottom = 4 },
},
spotlight = new SpotlightBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10, Bottom = 4 },
}
}
},
new FillFlowContainer
{
Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = 20 },
Children = new Drawable[]
{
artist = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
},
featuredArtist = new FeaturedArtistBeatmapBadge
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10 }
}
}
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = author = new AuthorInfo(),
},
beatmapAvailability = new BeatmapAvailability(),
new Container
{
RelativeSizeAxes = Axes.X,
Height = buttons_height,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
{
favouriteButton = new FavouriteButton
{
BeatmapSet = { BindTarget = BeatmapSet }
},
downloadButtonsContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
Spacing = new Vector2(buttons_spacing),
},
BeatmapSet = { BindTarget = BeatmapSet }
},
},
downloadButtonsContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
Spacing = new Vector2(buttons_spacing),
},
}
},
},
}
},
},
}
},
loading = new LoadingSpinner
{
@ -237,12 +192,17 @@ namespace osu.Game.Overlays.BeatmapSet
Picker.Beatmap.ValueChanged += b =>
{
Details.BeatmapInfo = b.NewValue;
externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineID}";
updateExternalLink();
onlineStatusPill.Status = b.NewValue?.Status ?? BeatmapOnlineStatus.None;
};
}
private void updateExternalLink()
{
if (externalLink != null) externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineID}#{Picker.Beatmap.Value?.Ruleset.ShortName}/{Picker.Beatmap.Value?.OnlineID}";
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@ -275,12 +235,38 @@ namespace osu.Game.Overlays.BeatmapSet
loading.Hide();
title.Text = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
artist.Text = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
var titleText = new RomanisableString(setInfo.NewValue.TitleUnicode, setInfo.NewValue.Title);
var artistText = new RomanisableString(setInfo.NewValue.ArtistUnicode, setInfo.NewValue.Artist);
explicitContent.Alpha = setInfo.NewValue.HasExplicitContent ? 1 : 0;
spotlight.Alpha = setInfo.NewValue.FeaturedInSpotlight ? 1 : 0;
featuredArtist.Alpha = setInfo.NewValue.TrackId != null ? 1 : 0;
title.Clear();
artist.Clear();
title.AddLink(titleText, LinkAction.SearchBeatmapSet, titleText);
title.AddArbitraryDrawable(Empty().With(d => d.Width = 5));
title.AddArbitraryDrawable(externalLink = new ExternalLinkButton());
if (setInfo.NewValue.HasExplicitContent)
{
title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
title.AddArbitraryDrawable(new ExplicitContentBeatmapBadge());
}
if (setInfo.NewValue.FeaturedInSpotlight)
{
title.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
title.AddArbitraryDrawable(new SpotlightBeatmapBadge());
}
artist.AddLink(artistText, LinkAction.SearchBeatmapSet, artistText);
if (setInfo.NewValue.TrackId != null)
{
artist.AddArbitraryDrawable(Empty().With(d => d.Width = 10));
artist.AddArbitraryDrawable(new FeaturedArtistBeatmapBadge());
}
updateExternalLink();
onlineStatusPill.FadeIn(500, Easing.OutQuint);
@ -327,5 +313,32 @@ namespace osu.Game.Overlays.BeatmapSet
break;
}
}
public partial class MetadataFlowContainer : LinkFlowContainer
{
public MetadataFlowContainer(Action<SpriteText> defaultCreationParameters = null)
: base(defaultCreationParameters)
{
TextAnchor = Anchor.CentreLeft;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
protected override DrawableLinkCompiler CreateLinkCompiler(ITextPart textPart) => new MetadataLinkCompiler(textPart);
public partial class MetadataLinkCompiler : DrawableLinkCompiler
{
public MetadataLinkCompiler(ITextPart part)
: base(part)
{
}
[BackgroundDependencyLoader]
private void load()
{
IdleColour = Color4.White;
}
}
}
}
}

View File

@ -3,14 +3,17 @@
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Overlays.BeatmapSet
{
@ -34,7 +37,10 @@ namespace osu.Game.Overlays.BeatmapSet
public Info()
{
MetadataSection source, tags, genre, language;
MetadataSectionNominators nominators;
MetadataSection source, tags;
MetadataSectionGenre genre;
MetadataSectionLanguage language;
OsuSpriteText notRankedPlaceholder;
RelativeSizeAxes = Axes.X;
@ -59,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapSet
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Child = new MetadataSection(MetadataType.Description),
Child = new MetadataSectionDescription(),
},
},
new Container
@ -76,12 +82,13 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Children = new[]
Children = new Drawable[]
{
source = new MetadataSection(MetadataType.Source),
genre = new MetadataSection(MetadataType.Genre) { Width = 0.5f },
language = new MetadataSection(MetadataType.Language) { Width = 0.5f },
tags = new MetadataSection(MetadataType.Tags),
nominators = new MetadataSectionNominators(),
source = new MetadataSectionSource(),
genre = new MetadataSectionGenre { Width = 0.5f },
language = new MetadataSectionLanguage { Width = 0.5f },
tags = new MetadataSectionTags(),
},
},
},
@ -118,10 +125,11 @@ namespace osu.Game.Overlays.BeatmapSet
BeatmapSet.ValueChanged += b =>
{
source.Text = b.NewValue?.Source ?? string.Empty;
tags.Text = b.NewValue?.Tags ?? string.Empty;
genre.Text = b.NewValue?.Genre.Name ?? string.Empty;
language.Text = b.NewValue?.Language.Name ?? string.Empty;
nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty<BeatmapSetOnlineNomination>(), b.NewValue?.RelatedUsers ?? Array.Empty<APIUser>());
source.Metadata = b.NewValue?.Source ?? string.Empty;
tags.Metadata = b.NewValue?.Tags ?? string.Empty;
genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified };
language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified };
bool setHasLeaderboard = b.NewValue?.Status > 0;
successRate.Alpha = setHasLeaderboard ? 1 : 0;
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;

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 osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
@ -11,26 +9,45 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSection : Container
public abstract partial class MetadataSection : MetadataSection<string>
{
public override string Metadata
{
set
{
if (string.IsNullOrEmpty(value))
{
this.FadeOut(TRANSITION_DURATION);
return;
}
base.Metadata = value;
}
}
protected MetadataSection(MetadataType type, Action<string>? searchAction = null)
: base(type, searchAction)
{
}
}
public abstract partial class MetadataSection<T> : Container
{
private readonly FillFlowContainer textContainer;
private readonly MetadataType type;
private TextFlowContainer textFlow;
private TextFlowContainer? textFlow;
private readonly Action<string> searchAction;
protected readonly Action<T>? SearchAction;
private const float transition_duration = 250;
protected const float TRANSITION_DURATION = 250;
public MetadataSection(MetadataType type, Action<string> searchAction = null)
protected MetadataSection(MetadataType type, Action<T>? searchAction = null)
{
this.type = type;
this.searchAction = searchAction;
SearchAction = searchAction;
Alpha = 0;
@ -53,7 +70,7 @@ namespace osu.Game.Overlays.BeatmapSet
AutoSizeAxes = Axes.Y,
Child = new OsuSpriteText
{
Text = this.type.GetLocalisableDescription(),
Text = type.GetLocalisableDescription(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
},
@ -61,23 +78,23 @@ namespace osu.Game.Overlays.BeatmapSet
};
}
public string Text
public virtual T Metadata
{
set
{
if (string.IsNullOrEmpty(value))
if (value == null)
{
this.FadeOut(transition_duration);
this.FadeOut(TRANSITION_DURATION);
return;
}
this.FadeIn(transition_duration);
this.FadeIn(TRANSITION_DURATION);
setTextAsync(value);
setTextFlowAsync(value);
}
}
private void setTextAsync(string text)
private void setTextFlowAsync(T metadata)
{
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
{
@ -88,44 +105,15 @@ namespace osu.Game.Overlays.BeatmapSet
{
textFlow?.Expire();
switch (type)
{
case MetadataType.Tags:
string[] tags = text.Split(" ");
for (int i = 0; i <= tags.Length - 1; i++)
{
string tag = tags[i];
if (searchAction != null)
loaded.AddLink(tag, () => searchAction(tag));
else
loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
if (i != tags.Length - 1)
loaded.AddText(" ");
}
break;
case MetadataType.Source:
if (searchAction != null)
loaded.AddLink(text, () => searchAction(text));
else
loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
break;
default:
loaded.AddText(text);
break;
}
AddMetadata(metadata, loaded);
textContainer.Add(textFlow = loaded);
// fade in if we haven't yet.
textContainer.FadeIn(transition_duration);
textContainer.FadeIn(TRANSITION_DURATION);
});
}
protected abstract void AddMetadata(T metadata, LinkFlowContainer loaded);
}
}

View File

@ -0,0 +1,21 @@
// 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.Game.Graphics.Containers;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionDescription : MetadataSection
{
public MetadataSectionDescription(Action<string>? searchAction = null)
: base(MetadataType.Description, searchAction)
{
}
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
{
loaded.AddText(metadata);
}
}
}

View File

@ -0,0 +1,30 @@
// 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.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionGenre : MetadataSection<BeatmapSetOnlineGenre>
{
public MetadataSectionGenre(Action<BeatmapSetOnlineGenre>? searchAction = null)
: base(MetadataType.Genre, searchAction)
{
}
protected override void AddMetadata(BeatmapSetOnlineGenre metadata, LinkFlowContainer loaded)
{
var genre = (SearchGenre)metadata.Id;
if (Enum.IsDefined(genre))
loaded.AddLink(genre.GetLocalisableDescription(), LinkAction.FilterBeatmapSetGenre, genre);
else
loaded.AddText(metadata.Name);
}
}
}

View File

@ -0,0 +1,30 @@
// 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.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osu.Game.Overlays.BeatmapListing;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionLanguage : MetadataSection<BeatmapSetOnlineLanguage>
{
public MetadataSectionLanguage(Action<BeatmapSetOnlineLanguage>? searchAction = null)
: base(MetadataType.Language, searchAction)
{
}
protected override void AddMetadata(BeatmapSetOnlineLanguage metadata, LinkFlowContainer loaded)
{
var language = (SearchLanguage)metadata.Id;
if (Enum.IsDefined(language))
loaded.AddLink(language.GetLocalisableDescription(), LinkAction.FilterBeatmapSetLanguage, language);
else
loaded.AddText(metadata.Name);
}
}
}

View File

@ -0,0 +1,63 @@
// 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.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionNominators : MetadataSection<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>
{
public override (BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) Metadata
{
set
{
if (value.CurrentNominations.Length == 0)
{
this.FadeOut(TRANSITION_DURATION);
return;
}
base.Metadata = value;
}
}
public MetadataSectionNominators(Action<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>? searchAction = null)
: base(MetadataType.Nominators, searchAction)
{
}
protected override void AddMetadata((BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) metadata, LinkFlowContainer loaded)
{
int[] nominatorIds = metadata.CurrentNominations.Select(n => n.UserId).ToArray();
int nominatorsFound = 0;
foreach (int nominatorId in nominatorIds)
{
foreach (var user in metadata.RelatedUsers)
{
if (nominatorId != user.OnlineID) continue;
nominatorsFound++;
loaded.AddUserLink(new APIUser
{
Username = user.Username,
Id = nominatorId,
});
if (nominatorsFound < nominatorIds.Length)
loaded.AddText(CommonStrings.ArrayAndWordsConnector);
break;
}
}
}
}
}

View File

@ -0,0 +1,25 @@
// 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.Game.Graphics.Containers;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionSource : MetadataSection
{
public MetadataSectionSource(Action<string>? searchAction = null)
: base(MetadataType.Source, searchAction)
{
}
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
{
if (SearchAction != null)
loaded.AddLink(metadata, () => SearchAction(metadata));
else
loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, metadata);
}
}
}

View File

@ -0,0 +1,35 @@
// 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.Game.Graphics.Containers;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionTags : MetadataSection
{
public MetadataSectionTags(Action<string>? searchAction = null)
: base(MetadataType.Tags, searchAction)
{
}
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
{
string[] tags = metadata.Split(" ");
for (int i = 0; i <= tags.Length - 1; i++)
{
string tag = tags[i];
if (SearchAction != null)
loaded.AddLink(tag, () => SearchAction(tag));
else
loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
if (i != tags.Length - 1)
loaded.AddText(" ");
}
}
}
}

View File

@ -23,6 +23,9 @@ namespace osu.Game.Overlays.BeatmapSet
Genre,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoLanguage))]
Language
Language,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoNominators))]
Nominators,
}
}

View File

@ -95,8 +95,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersScore, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy, Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, minSize: 60, maxSize: 70)),
new TableColumn("", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 25)), // flag
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 125)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120))
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersPlayer, Anchor.CentreLeft, new Dimension(minSize: 125)),
new TableColumn(BeatmapsetsStrings.ShowScoreboardHeadersCombo, Anchor.CentreLeft, new Dimension(minSize: 70, maxSize: 120))
};
// All statistics across all scores, unordered.
@ -116,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
var displayName = ruleset.GetDisplayNameForHitResult(result);
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
}

View File

@ -128,9 +128,8 @@ namespace osu.Game.Overlays.Chat
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
searchIconContainer.FadeTo(showSearch ? 1 : 0);
// Clear search terms if any exist when switching back to chat mode
if (!showSearch)
OnSearchTermsChanged?.Invoke(string.Empty);
if (showSearch)
OnSearchTermsChanged?.Invoke(chatTextBox.Current.Value);
}, true);
currentChannel.BindValueChanged(change =>
@ -151,6 +150,12 @@ namespace osu.Game.Overlays.Chat
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
break;
}
if (change.OldValue != null)
chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage);
if (newChannel != null)
chatTextBox.Current.BindTo(newChannel.TextBoxMessage);
}, true);
}

View File

@ -24,7 +24,6 @@ namespace osu.Game.Overlays.Chat
bool showSearch = change.NewValue;
PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
Text = string.Empty;
}, true);
}

View File

@ -17,9 +17,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
@ -148,11 +150,11 @@ namespace osu.Game.Overlays.Chat
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem("View Profile", MenuItemType.Highlighted, openUserProfile)
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)
};
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, openUserChannel));
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
return items.ToArray();
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Chat.Listing
private Box hoverBox = null!;
private SpriteIcon checkbox = null!;
private OsuSpriteText channelText = null!;
private OsuSpriteText topicText = null!;
private OsuTextFlowContainer topicText = null!;
private IBindable<bool> channelJoined = null!;
[Resolved]
@ -65,8 +65,8 @@ namespace osu.Game.Overlays.Chat.Listing
Masking = true;
CornerRadius = 5;
RelativeSizeAxes = Axes.X;
Height = 20 + (vertical_margin * 2);
RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X;
AutoSizeAxes = Content.AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
@ -79,14 +79,19 @@ namespace osu.Game.Overlays.Chat.Listing
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 40),
new Dimension(GridSizeMode.Absolute, 200),
new Dimension(GridSizeMode.Absolute, 400),
new Dimension(maxSize: 400),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50), // enough for any 5 digit user count
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize, minSize: 20 + (vertical_margin * 2)),
},
Content = new[]
{
@ -108,12 +113,13 @@ namespace osu.Game.Overlays.Chat.Listing
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
Margin = new MarginPadding { Bottom = 2 },
},
topicText = new OsuSpriteText
topicText = new OsuTextFlowContainer(t => t.Font = OsuFont.Torus.With(size: text_size))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Channel.Topic,
Font = OsuFont.Torus.With(size: text_size),
Margin = new MarginPadding { Bottom = 2 },
},
new SpriteIcon

View File

@ -22,6 +22,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Screens.Edit.Setup;
@ -127,14 +128,17 @@ namespace osu.Game.Overlays.FirstRunSetup
if (available)
{
copyInformation.Text =
"Data migration will use \"hard links\". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation.";
"Data migration will use \"hard links\". No extra disk space will be used, and you can delete either data folder at any point without affecting the other installation. ";
copyInformation.AddLink("Learn more about how \"hard links\" work", LinkAction.OpenWiki, @"Client/Release_stream/Lazer/File_storage#via-hard-links");
}
else if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
else if (!RuntimeInfo.IsDesktop)
copyInformation.Text = "Lightweight linking of files is not supported on your operating system yet, so a copy of all files will be made during import.";
else
{
copyInformation.Text =
"A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS). ";
copyInformation.Text = RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system is NTFS). "
: "A second copy of all files will be made during import. To avoid this, please make sure the lazer data folder is on the same drive as your previous osu! install (and the file system supports hard links). ";
copyInformation.AddLink(GeneralSettingsStrings.ChangeFolderLocation, () =>
{
game?.PerformFromScreen(menu => menu.Push(new MigrationSelectScreen()));

View File

@ -79,8 +79,7 @@ namespace osu.Game.Overlays.FirstRunSetup
Direction = FillDirection.Full;
Spacing = new Vector2(5);
ChildrenEnumerable = Enum.GetValues(typeof(Language))
.Cast<Language>()
ChildrenEnumerable = Enum.GetValues<Language>()
.Select(l => new LanguageButton(l)
{
Action = () => frameworkLocale.Value = l.ToCultureCode()

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@ -38,20 +39,30 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
Child = new OsuContextMenuContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
AutoSizeAxes = Axes.Y,
Child = new PopoverContainer
{
Header.With(h => h.Depth = float.MinValue),
content = new PopoverContainer
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h => h.Depth = float.MinValue),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
}
},
}
},
Loading = new LoadingLayer(true)

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 Humanizer;
using osu.Framework.Allocation;
@ -25,15 +23,15 @@ namespace osu.Game.Overlays.Profile.Header
{
public partial class BottomHeaderContainer : CompositeDrawable
{
public readonly Bindable<APIUser> User = new Bindable<APIUser>();
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
private LinkFlowContainer topLinkContainer;
private LinkFlowContainer bottomLinkContainer;
private LinkFlowContainer topLinkContainer = null!;
private LinkFlowContainer bottomLinkContainer = null!;
private Color4 iconColour;
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
public BottomHeaderContainer()
{
@ -78,7 +76,7 @@ namespace osu.Game.Overlays.Profile.Header
User.BindValueChanged(user => updateDisplay(user.NewValue));
}
private void updateDisplay(APIUser user)
private void updateDisplay(APIUser? user)
{
topLinkContainer.Clear();
bottomLinkContainer.Clear();
@ -164,7 +162,7 @@ namespace osu.Game.Overlays.Profile.Header
private void addSpacer(OsuTextFlowContainer textFlow) => textFlow.AddArbitraryDrawable(new Container { Width = 15 });
private bool tryAddInfo(IconUsage icon, string content, string link = null)
private bool tryAddInfo(IconUsage icon, string content, string? link = null)
{
if (string.IsNullOrEmpty(content)) return false;

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
@ -20,10 +18,10 @@ namespace osu.Game.Overlays.Profile.Header
public partial class CentreHeaderContainer : CompositeDrawable
{
public readonly BindableBool DetailsVisible = new BindableBool(true);
public readonly Bindable<APIUser> User = new Bindable<APIUser>();
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
private OverlinedInfoContainer hiddenDetailGlobal;
private OverlinedInfoContainer hiddenDetailCountry;
private OverlinedInfoContainer hiddenDetailGlobal = null!;
private OverlinedInfoContainer hiddenDetailCountry = null!;
public CentreHeaderContainer()
{
@ -146,7 +144,7 @@ namespace osu.Game.Overlays.Profile.Header
User.BindValueChanged(user => updateDisplay(user.NewValue));
}
private void updateDisplay(APIUser user)
private void updateDisplay(APIUser? user)
{
hiddenDetailGlobal.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
hiddenDetailCountry.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";

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 osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -23,9 +21,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
public override LocalisableString TooltipText => DetailsVisible.Value ? CommonStrings.ButtonsCollapse : CommonStrings.ButtonsExpand;
private SpriteIcon icon;
private Sample sampleOpen;
private Sample sampleClose;
private SpriteIcon icon = null!;
private Sample? sampleOpen;
private Sample? sampleClose;
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
@ -14,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class FollowersButton : ProfileHeaderStatisticsButton
{
public readonly Bindable<APIUser> User = new Bindable<APIUser>();
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public override LocalisableString TooltipText => FriendsStrings.ButtonsDisabled;

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -20,11 +18,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class LevelBadge : CompositeDrawable, IHasTooltip
{
public readonly Bindable<APIUser> User = new Bindable<APIUser>();
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public LocalisableString TooltipText { get; private set; }
private OsuSpriteText levelText;
private OsuSpriteText levelText = null!;
public LevelBadge()
{
@ -53,10 +51,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
User.BindValueChanged(user => updateLevel(user.NewValue));
}
private void updateLevel(APIUser user)
private void updateLevel(APIUser? user)
{
levelText.Text = user?.Statistics?.Level.Current.ToString() ?? "0";
TooltipText = UsersStrings.ShowStatsLevel(user?.Statistics?.Level.Current.ToString());
string level = user?.Statistics?.Level.Current.ToString() ?? "0";
levelText.Text = level;
TooltipText = UsersStrings.ShowStatsLevel(level);
}
}
}

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
@ -21,12 +19,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class LevelProgressBar : CompositeDrawable, IHasTooltip
{
public readonly Bindable<APIUser> User = new Bindable<APIUser>();
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public LocalisableString TooltipText { get; }
private Bar levelProgressBar;
private OsuSpriteText levelProgressText;
private Bar levelProgressBar = null!;
private OsuSpriteText levelProgressText = null!;
public LevelProgressBar()
{
@ -61,7 +59,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
User.BindValueChanged(user => updateProgress(user.NewValue));
}
private void updateProgress(APIUser user)
private void updateProgress(APIUser? user)
{
levelProgressBar.Length = user?.Statistics?.Level.Progress / 100f ?? 0;
levelProgressText.Text = user?.Statistics?.Level.Progress.ToLocalisableString("0'%'") ?? default;

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