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

Merge branch 'ppy:master' into master

This commit is contained in:
Nostril 2022-06-08 19:29:14 -07:00 committed by GitHub
commit 5cbd3bd67e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
146 changed files with 2705 additions and 1240 deletions

View File

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

View File

@ -52,10 +52,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.527.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.530.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.605.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.11.2" />
<PackageReference Include="Realm" Version="10.14.0" />
</ItemGroup>
</Project>

View File

@ -14,11 +14,11 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, 151, "diffcalc-test")]
[TestCase(2.3449735700206298d, 242, "diffcalc-test")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, 151, "diffcalc-test")]
[TestCase(2.7879104989252959d, 242, "diffcalc-test")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
foreach (var v in base.ToDatabaseAttributes())
yield return v;
// Todo: osu!mania doesn't output MaxCombo attribute for some reason.
yield return (ATTRIB_ID_MAX_COMBO, MaxCombo);
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_SCORE_MULTIPLIER, ScoreMultiplier);
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
base.FromDatabaseAttributes(values);
MaxCombo = (int)values[ATTRIB_ID_MAX_COMBO];
StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
ScoreMultiplier = values[ATTRIB_ID_SCORE_MULTIPLIER];

View File

@ -52,10 +52,18 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
ScoreMultiplier = getScoreMultiplier(mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject)
};
}
private static int maxComboForObject(HitObject hitObject)
{
if (hitObject is HoldNote hold)
return 1 + (int)((hold.EndTime - hold.StartTime) / 100);
return 1;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
var sortedObjects = beatmap.HitObjects.ToArray();

View File

@ -79,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
Result = { BindTarget = SpinsPerMinute },
},
ticks = new Container<DrawableSpinnerTick>(),
ticks = new Container<DrawableSpinnerTick>
{
RelativeSizeAxes = Axes.Both,
},
new AspectContainer
{
Anchor = Anchor.Centre,

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
@ -10,13 +12,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject;
public DrawableSpinnerTick()
: base(null)
: this(null)
{
}
public DrawableSpinnerTick(SpinnerTick spinnerTick)
: base(spinnerTick)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration;

View File

@ -69,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime, Position = Position }
: new SpinnerBonusTick { StartTime = startTime, Position = Position });
? new SpinnerTick { StartTime = startTime }
: new SpinnerBonusTick { StartTime = startTime });
}
}

View File

@ -78,7 +78,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
});
if (!(source.FindProvider(s => s.GetTexture("spinner-top") != null) is DefaultLegacySkin))
var topProvider = source.FindProvider(s => s.GetTexture("spinner-top") != null);
if (topProvider is LegacySkinTransformer transformer && !(transformer.Skin is DefaultLegacySkin))
{
AddInternal(ApproachCircle = new Sprite
{

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
@ -322,12 +323,14 @@ namespace osu.Game.Rulesets.Taiko.UI
private class ProxyContainer : LifetimeManagementContainer
{
public new MarginPadding Padding
{
set => base.Padding = value;
}
public void Add(Drawable proxy) => AddInternal(proxy);
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds)
{
// DrawableHitObject disables masking.
// Hitobject content is proxied and unproxied based on hit status and the IsMaskedAway value could get stuck because of this.
return false;
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Chat
[SetUp]
public void Setup() => Schedule(() =>
{
var container = new ChannelManagerContainer();
var container = new ChannelManagerContainer(API);
Child = container;
channelManager = container.ChannelManager;
});
@ -145,11 +145,11 @@ namespace osu.Game.Tests.Chat
private class ChannelManagerContainer : CompositeDrawable
{
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
public ChannelManager ChannelManager { get; }
public ChannelManagerContainer()
public ChannelManagerContainer(IAPIProvider apiProvider)
{
InternalChild = ChannelManager;
InternalChild = ChannelManager = new ChannelManager(apiProvider);
}
}
}

View File

@ -710,7 +710,7 @@ namespace osu.Game.Tests.Database
var imported = await LoadOszIntoStore(importer, realm.Realm);
realm.Realm.Write(() =>
await realm.Realm.WriteAsync(() =>
{
foreach (var b in imported.Beatmaps)
b.OnlineID = -1;

View File

@ -59,30 +59,34 @@ namespace osu.Game.Tests.Gameplay
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
// No header shouldn't cause any change
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame());
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame());
Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(1_000_000));
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(1));
// Reset with a miss instead.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int> { { HitResult.Miss, 1 } }, DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.EqualTo(1));
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
// Reset with no judged hit.
scoreProcessor.ResetFromReplayFrame(new OsuRuleset(), new OsuReplayFrame
scoreProcessor.ResetFromReplayFrame(new OsuReplayFrame
{
Header = new FrameHeader(0, 0, 0, new Dictionary<HitResult, int>(), DateTimeOffset.Now)
});
Assert.That(scoreProcessor.TotalScore.Value, Is.Zero);
Assert.That(scoreProcessor.JudgedHits, Is.Zero);
Assert.That(scoreProcessor.Combo.Value, Is.EqualTo(0));
}
private class TestJudgement : Judgement

View File

@ -59,11 +59,13 @@ namespace osu.Game.Tests.Skins
AddAssert("Check float parse lookup", () => requester.GetConfig<string, float>("FloatTest")?.Value == 1.1f);
}
[Test]
public void TestBoolLookup()
[TestCase("0", false)]
[TestCase("1", true)]
[TestCase("2", true)] // https://github.com/ppy/osu/issues/18579
public void TestBoolLookup(string originalValue, bool expectedParsedValue)
{
AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = "1");
AddAssert("Check bool parse lookup", () => requester.GetConfig<string, bool>("BoolTest")?.Value == true);
AddStep("Add config values", () => userSource.Configuration.ConfigDictionary["BoolTest"] = originalValue);
AddAssert("Check bool parse lookup", () => requester.GetConfig<string, bool>("BoolTest")?.Value == expectedParsedValue);
}
[Test]

View File

@ -0,0 +1,48 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Timing;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneTapButton : OsuManualInputManagerTestScene
{
private TapButton tapButton;
[Cached]
private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Test]
public void TestBasic()
{
AddStep("create button", () =>
{
Child = tapButton = new TapButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(4),
};
});
bool pressed = false;
AddRepeatStep("Press button", () =>
{
InputManager.MoveMouseTo(tapButton);
if (!pressed)
InputManager.PressButton(MouseButton.Left);
else
InputManager.ReleaseButton(MouseButton.Left);
pressed = !pressed;
}, 100);
}
}
}

View File

@ -11,7 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Timing;
@ -77,34 +77,6 @@ namespace osu.Game.Tests.Visual.Editing
timingInfo.Text = $"offset: {selectedGroup.Value.Time:N2} bpm: {selectedGroup.Value.ControlPoints.OfType<TimingControlPoint>().First().BPM:N2}";
}
[Test]
public void TestNoop()
{
AddStep("do nothing", () => { });
}
[Test]
public void TestTapThenReset()
{
AddStep("click tap button", () =>
{
control.ChildrenOfType<RoundedButton>()
.Last()
.TriggerClick();
});
AddUntilStep("wait for track playing", () => Clock.IsRunning);
AddStep("click reset button", () =>
{
control.ChildrenOfType<RoundedButton>()
.First()
.TriggerClick();
});
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}
[Test]
public void TestBasic()
{
@ -115,7 +87,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("click tap button", () =>
{
control.ChildrenOfType<RoundedButton>()
control.ChildrenOfType<OsuButton>()
.Last()
.TriggerClick();
});
@ -129,6 +101,28 @@ namespace osu.Game.Tests.Visual.Editing
});
}
[Test]
public void TestTapThenReset()
{
AddStep("click tap button", () =>
{
control.ChildrenOfType<OsuButton>()
.Last()
.TriggerClick();
});
AddUntilStep("wait for track playing", () => Clock.IsRunning);
AddStep("click reset button", () =>
{
control.ChildrenOfType<OsuButton>()
.First()
.TriggerClick();
});
AddUntilStep("wait for track stopped", () => !Clock.IsRunning);
}
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;

View File

@ -0,0 +1,48 @@
// 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.Framework.Utils;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneTimelineZoom : TimelineTestScene
{
public override Drawable CreateTestComponent() => Empty();
[Test]
public void TestVisibleRangeUpdatesOnZoomChange()
{
double initialVisibleRange = 0;
AddStep("reset zoom", () => TimelineArea.Timeline.Zoom = 100);
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale zoom", () => TimelineArea.Timeline.Zoom = 200);
AddAssert("range halved", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange / 2, 1));
AddStep("descale zoom", () => TimelineArea.Timeline.Zoom = 50);
AddAssert("range doubled", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange * 2, 1));
AddStep("restore zoom", () => TimelineArea.Timeline.Zoom = 100);
AddAssert("range restored", () => Precision.AlmostEquals(TimelineArea.Timeline.VisibleRange, initialVisibleRange, 1));
}
[Test]
public void TestVisibleRangeConstantOnSizeChange()
{
double initialVisibleRange = 0;
AddStep("reset timeline size", () => TimelineArea.Timeline.Width = 1);
AddStep("get initial range", () => initialVisibleRange = TimelineArea.Timeline.VisibleRange);
AddStep("scale timeline size", () => TimelineArea.Timeline.Width = 2);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
AddStep("descale timeline size", () => TimelineArea.Timeline.Width = 0.5f);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
AddStep("restore timeline size", () => TimelineArea.Timeline.Width = 1);
AddAssert("same range", () => TimelineArea.Timeline.VisibleRange == initialVisibleRange);
}
}
}

View File

@ -44,7 +44,12 @@ namespace osu.Game.Tests.Visual.Editing
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(30)
},
scrollContainer = new ZoomableScrollContainer { RelativeSizeAxes = Axes.Both }
scrollContainer = new ZoomableScrollContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
}
}
},
new MenuCursor()
@ -62,7 +67,15 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestWidthInitialization()
{
AddAssert("Inner container width was initialized", () => innerBox.DrawWidth > 0);
AddAssert("Inner container width was initialized", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]
public void TestWidthUpdatesOnDrawSizeChanges()
{
AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f);
AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2);
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}
[Test]

View File

@ -18,7 +18,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
@ -27,7 +26,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public abstract class MultiplayerGameplayLeaderboardTestScene : OsuTestScene
{
private const int total_users = 16;
protected const int TOTAL_USERS = 16;
protected readonly BindableList<MultiplayerRoomUser> MultiplayerUsers = new BindableList<MultiplayerRoomUser>();
@ -35,9 +34,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected virtual MultiplayerRoomUser CreateUser(int userId) => new MultiplayerRoomUser(userId);
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor);
protected abstract MultiplayerGameplayLeaderboard CreateLeaderboard();
private readonly BindableList<int> multiplayerUserIds = new BindableList<int>();
private readonly BindableDictionary<int, SpectatorState> watchedUserStates = new BindableDictionary<int, SpectatorState>();
private OsuConfigManager config;
@ -81,6 +81,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerClient.SetupGet(c => c.CurrentMatchPlayingUserIds)
.Returns(() => multiplayerUserIds);
spectatorClient.SetupGet(c => c.WatchedUserStates)
.Returns(() => watchedUserStates);
}
[SetUpSteps]
@ -100,8 +103,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("populate users", () =>
{
MultiplayerUsers.Clear();
for (int i = 0; i < total_users; i++)
MultiplayerUsers.Add(CreateUser(i));
for (int i = 0; i < TOTAL_USERS; i++)
{
var user = CreateUser(i);
MultiplayerUsers.Add(user);
watchedUserStates[i] = new SpectatorState
{
BeatmapID = 0,
RulesetID = 0,
Mods = user.Mods,
MaximumScoringValues = new ScoringValues
{
BaseScore = 10000,
MaxCombo = 1000,
CountBasicHitObjects = 1000
}
};
}
});
AddStep("create leaderboard", () =>
@ -109,13 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Leaderboard?.Expire();
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
OsuScoreProcessor scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playableBeatmap);
Child = scoreProcessor;
LoadComponentAsync(Leaderboard = CreateLeaderboard(scoreProcessor), Add);
LoadComponentAsync(Leaderboard = CreateLeaderboard(), Add);
});
AddUntilStep("wait for load", () => Leaderboard.IsLoaded);

View File

@ -4,11 +4,11 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play.HUD;
@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("reset", () =>
{
Clear();
leaderboard?.RemoveAndDisposeImmediately();
clocks = new Dictionary<int, ManualClock>
{
@ -32,21 +32,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ PLAYER_2_ID, new ManualClock() }
};
foreach ((int userId, var _) in clocks)
foreach ((int userId, _) in clocks)
{
SpectatorClient.SendStartPlay(userId, 0);
OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId });
OnlinePlayDependencies.MultiplayerClient.AddUser(new APIUser { Id = userId }, true);
}
});
AddStep("create leaderboard", () =>
{
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value);
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{
Expanded = { Value = true }
}, Add);

View File

@ -1,22 +1,58 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play.HUD;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerGameplayLeaderboardTestScene
{
protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor)
protected override MultiplayerRoomUser CreateUser(int userId)
{
return new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
var user = base.CreateUser(userId);
if (userId == TOTAL_USERS - 1)
user.Mods = new[] { new APIMod(new OsuModNoFail()) };
return user;
}
protected override MultiplayerGameplayLeaderboard CreateLeaderboard()
{
return new TestLeaderboard(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
[Test]
public void TestPerUserMods()
{
AddStep("first user has no mods", () => Assert.That(((TestLeaderboard)Leaderboard).UserMods[0], Is.Empty));
AddStep("last user has NF mod", () =>
{
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1], Has.One.Items);
Assert.That(((TestLeaderboard)Leaderboard).UserMods[TOTAL_USERS - 1].Single(), Is.TypeOf<OsuModNoFail>());
});
}
private class TestLeaderboard : MultiplayerGameplayLeaderboard
{
public Dictionary<int, IReadOnlyList<Mod>> UserMods => UserScores.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ScoreProcessor.Mods);
public TestLeaderboard(MultiplayerRoomUser[] users)
: base(users)
{
}
}
}
}

View File

@ -5,7 +5,6 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play.HUD;
@ -25,8 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
return user;
}
protected override MultiplayerGameplayLeaderboard CreateLeaderboard(OsuScoreProcessor scoreProcessor) =>
new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, MultiplayerUsers.ToArray())
protected override MultiplayerGameplayLeaderboard CreateLeaderboard() =>
new MultiplayerGameplayLeaderboard(MultiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -609,8 +609,6 @@ namespace osu.Game.Tests.Visual.Navigation
public ModSelectOverlay ModSelectOverlay => ModSelect;
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
protected override bool DisplayStableImportPrompt => false;
}
}
}

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online
{
linkColour = colours.Blue;
var chatManager = new ChannelManager();
var chatManager = new ChannelManager(API);
BindableList<Channel> availableChannels = (BindableList<Channel>)chatManager.AvailableChannels;
availableChannels.Add(new Channel { Name = "#english" });
availableChannels.Add(new Channel { Name = "#japanese" });

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(ChannelManager), channelManager = new ChannelManager()),
(typeof(ChannelManager), channelManager = new ChannelManager(API)),
},
Children = new Drawable[]
{
@ -572,15 +572,15 @@ namespace osu.Game.Tests.Visual.Online
public SlowLoadingDrawableChannel GetSlowLoadingChannel(Channel channel) => DrawableChannels.OfType<SlowLoadingDrawableChannel>().Single(c => c.Channel == channel);
protected override ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel)
protected override DrawableChannel CreateDrawableChannel(Channel newChannel)
{
return SlowLoading
? new SlowLoadingDrawableChannel(newChannel)
: new ChatOverlayDrawableChannel(newChannel);
: new DrawableChannel(newChannel);
}
}
private class SlowLoadingDrawableChannel : ChatOverlayDrawableChannel
private class SlowLoadingDrawableChannel : DrawableChannel
{
public readonly ManualResetEventSlim LoadEvent = new ManualResetEventSlim();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online
Schedule(() =>
{
Child = testContainer = new TestContainer(new[] { publicChannel, privateMessageChannel })
Child = testContainer = new TestContainer(API, new[] { publicChannel, privateMessageChannel })
{
RelativeSizeAxes = Axes.Both,
};
@ -178,6 +179,36 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("1 notification fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 1);
}
/// <summary>
/// Ensures that <see cref="MessageNotifier"/> handles channels which have not been or could not be resolved (i.e. <see cref="Channel.Id"/> = 0).
/// </summary>
[Test]
public void TestSendInUnresolvedChannel()
{
int i = 1;
Channel unresolved = null;
AddRepeatStep("join unresolved channels", () => testContainer.ChannelManager.JoinChannel(unresolved = new Channel(new APIUser
{
Id = 100 + i,
Username = $"Foreign #{i++}",
})), 5);
AddStep("send message in unresolved channel", () =>
{
Debug.Assert(unresolved.Id == 0);
unresolved.AddLocalEcho(new LocalEchoMessage
{
Sender = API.LocalUser.Value,
ChannelId = unresolved.Id,
Content = "Some message",
});
});
AddAssert("no notifications fired", () => testContainer.NotificationOverlay.UnreadCount.Value == 0);
}
private void receiveMessage(APIUser sender, Channel channel, string content) => channel.AddNewMessages(createMessage(sender, channel, content));
private Message createMessage(APIUser sender, Channel channel, string content) => new Message(messageIdCounter++)
@ -198,7 +229,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestContainer : Container
{
[Cached]
public ChannelManager ChannelManager { get; } = new ChannelManager();
public ChannelManager ChannelManager { get; }
[Cached(typeof(INotificationOverlay))]
public NotificationOverlay NotificationOverlay { get; } = new NotificationOverlay
@ -214,9 +245,10 @@ namespace osu.Game.Tests.Visual.Online
private readonly Channel[] channels;
public TestContainer(Channel[] channels)
public TestContainer(IAPIProvider api, Channel[] channels)
{
this.channels = channels;
ChannelManager = new ChannelManager(api);
}
[BackgroundDependencyLoader]

View File

@ -11,6 +11,7 @@ using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat;
using osuTK.Input;
@ -44,17 +45,22 @@ namespace osu.Game.Tests.Visual.Online
Id = 5,
};
[Cached]
private ChannelManager channelManager = new ChannelManager();
private ChannelManager channelManager;
private TestStandAloneChatDisplay chatDisplay;
private int messageIdSequence;
private Channel testChannel;
public TestSceneStandAloneChatDisplay()
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
Add(channelManager);
Add(channelManager = new ChannelManager(parent.Get<IAPIProvider>()));
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(channelManager);
return dependencies;
}
[SetUp]
@ -128,11 +134,11 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Ensure no adjacent day separators", () =>
{
var indices = chatDisplay.FillFlow.OfType<DrawableChannel.DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
var indices = chatDisplay.FillFlow.OfType<DaySeparator>().Select(ds => chatDisplay.FillFlow.IndexOf(ds));
foreach (int i in indices)
{
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DrawableChannel.DaySeparator)
if (i < chatDisplay.FillFlow.Count && chatDisplay.FillFlow[i + 1] is DaySeparator)
return false;
}

View File

@ -96,6 +96,7 @@ namespace osu.Game.Tests.Visual.Ranking
beatmap.Metadata.Author = author;
beatmap.Metadata.Title = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap title";
beatmap.Metadata.Artist = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong beatmap artist";
beatmap.DifficultyName = "Verrrrrrrrrrrrrrrrrrry looooooooooooooooooooooooong difficulty name";
return beatmap;
}

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.Settings
AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
}
@ -129,16 +130,18 @@ namespace osu.Game.Tests.Visual.Settings
SettingsNumberBox numberBox = null;
AddStep("create settings item", () => Child = numberBox = new SettingsNumberBox());
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<SettingsNoticeText>().Any());
AddAssert("warning text not created", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text", () => numberBox.WarningText = "this is a warning!");
AddAssert("warning text created", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
AddStep("set warning text", () => numberBox.SetNoticeText("this is a warning!", true));
AddAssert("warning text created", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("unset warning text", () => numberBox.WarningText = default);
AddAssert("warning text hidden", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 0);
AddStep("unset warning text", () => numberBox.ClearNoticeText());
AddAssert("warning text hidden", () => !numberBox.ChildrenOfType<LinkFlowContainer>().Any());
AddStep("set warning text again", () => numberBox.WarningText = "another warning!");
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<SettingsNoticeText>().Single().Alpha == 1);
AddStep("set warning text again", () => numberBox.SetNoticeText("another warning!", true));
AddAssert("warning text shown again", () => numberBox.ChildrenOfType<LinkFlowContainer>().Single().Alpha == 1);
AddStep("set non warning text", () => numberBox.SetNoticeText("you did good!"));
}
}
}

View File

@ -5,12 +5,11 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
@ -23,38 +22,28 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneBeatmapRecommendations : OsuGameTestScene
{
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps]
public override void SetUpSteps()
{
AddStep("register request handling", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(getUser(userRequest.Ruleset.OnlineID));
return true;
}
return false;
};
});
base.SetUpSteps();
APIUser getUser(int? rulesetID)
AddStep("populate ruleset statistics", () =>
{
return new APIUser
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>();
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{
Username = @"Dummy",
Id = 1001,
Statistics = new UserStatistics
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics
{
PP = getNecessaryPP(rulesetID)
}
};
}
PP = getNecessaryPP(rulesetInfo.OnlineID)
};
});
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics;
});
decimal getNecessaryPP(int? rulesetID)
{

View File

@ -18,6 +18,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
@ -80,6 +81,37 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("delete all beatmaps", () => manager?.Delete());
}
[Test]
public void TestPlaceholderBeatmapPresence()
{
createSongSelect();
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
addRulesetImportStep(0);
AddUntilStep("wait for placeholder hidden", () => getPlaceholder()?.State.Value == Visibility.Hidden);
AddStep("delete all beatmaps", () => manager?.Delete());
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
}
[Test]
public void TestPlaceholderConvertSetting()
{
changeRuleset(2);
addRulesetImportStep(0);
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
createSongSelect();
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Visible);
AddStep("click link in placeholder", () => getPlaceholder().ChildrenOfType<DrawableLinkCompiler>().First().TriggerClick());
AddUntilStep("convert setting changed", () => config.Get<bool>(OsuSetting.ShowConvertedBeatmaps));
AddUntilStep("wait for placeholder visible", () => getPlaceholder()?.State.Value == Visibility.Hidden);
}
[Test]
public void TestSingleFilterOnEnter()
{
@ -941,6 +973,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private int getBeatmapIndex(BeatmapSetInfo set, BeatmapInfo info) => set.Beatmaps.IndexOf(info);
private NoResultsPlaceholder getPlaceholder() => songSelect.ChildrenOfType<NoResultsPlaceholder>().FirstOrDefault();
private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmapInfo);
private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon)

View File

@ -5,6 +5,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
using osu.Game.Tournament.IPC;
@ -29,7 +30,7 @@ namespace osu.Game.Tournament.Components
}
[BackgroundDependencyLoader(true)]
private void load(MatchIPCInfo ipc)
private void load(MatchIPCInfo ipc, IAPIProvider api)
{
if (ipc != null)
{
@ -45,7 +46,7 @@ namespace osu.Game.Tournament.Components
if (manager == null)
{
AddInternal(manager = new ChannelManager { HighPollRate = { Value = true } });
AddInternal(manager = new ChannelManager(api) { HighPollRate = { Value = true } });
Channel.BindTo(manager.CurrentChannel);
}

View File

@ -319,6 +319,15 @@ namespace osu.Game.Beatmaps
});
}
public void DeleteAllVideos()
{
realm.Write(r =>
{
var items = r.All<BeatmapSetInfo>().Where(s => !s.DeletePending && !s.Protected);
beatmapModelManager.DeleteVideos(items.ToList());
});
}
public void UndeleteAll()
{
realm.Run(r => beatmapModelManager.Undelete(r.All<BeatmapSetInfo>().Where(s => s.DeletePending).ToList()));

View File

@ -16,6 +16,7 @@ using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Skinning;
using osu.Game.Stores;
using osu.Game.Overlays.Notifications;
#nullable enable
@ -33,6 +34,8 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
public static readonly string[] VIDEO_EXTENSIONS = { ".mp4", ".mov", ".avi", ".flv" };
public BeatmapModelManager(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
: base(realm, storage, onlineLookupQueue)
{
@ -114,5 +117,50 @@ namespace osu.Game.Beatmaps
item.CopyChangesToRealm(existing);
});
}
/// <summary>
/// Delete videos from a list of beatmaps.
/// This will post notifications tracking progress.
/// </summary>
public void DeleteVideos(List<BeatmapSetInfo> items, bool silent = false)
{
if (items.Count == 0) return;
var notification = new ProgressNotification
{
Progress = 0,
Text = $"Preparing to delete all {HumanisedModelName} videos...",
CompletionText = "No videos found to delete!",
State = ProgressNotificationState.Active,
};
if (!silent)
PostNotification?.Invoke(notification);
int i = 0;
int deleted = 0;
foreach (var b in items)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
var video = b.Files.FirstOrDefault(f => VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
if (video != null)
{
DeleteFile(b, video);
deleted++;
notification.CompletionText = $"Deleted {deleted} {HumanisedModelName} video(s)!";
}
notification.Text = $"Deleting videos from {HumanisedModelName}s ({deleted} deleted)";
notification.Progress = (float)++i / items.Count;
}
notification.State = ProgressNotificationState.Completed;
}
}
}

View File

@ -7,11 +7,8 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
namespace osu.Game.Beatmaps
@ -25,26 +22,15 @@ namespace osu.Game.Beatmaps
[Resolved]
private IAPIProvider api { get; set; }
[Resolved]
private IRulesetStore rulesets { get; set; }
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
/// <summary>
/// The user for which the last requests were run.
/// </summary>
private int? requestedUserId;
private readonly Dictionary<IRulesetInfo, double> recommendedDifficultyMapping = new Dictionary<IRulesetInfo, double>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly Dictionary<string, double> recommendedDifficultyMapping = new Dictionary<string, double>();
[BackgroundDependencyLoader]
private void load()
{
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
api.LocalUser.BindValueChanged(_ => populateValues(), true);
}
/// <summary>
@ -58,12 +44,12 @@ namespace osu.Game.Beatmaps
[CanBeNull]
public BeatmapInfo GetRecommendedBeatmap(IEnumerable<BeatmapInfo> beatmaps)
{
foreach (var r in orderedRulesets)
foreach (string r in orderedRulesets)
{
if (!recommendedDifficultyMapping.TryGetValue(r, out double recommendation))
continue;
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b =>
BeatmapInfo beatmapInfo = beatmaps.Where(b => b.Ruleset.ShortName.Equals(r)).OrderBy(b =>
{
double difference = b.StarRating - recommendation;
return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder
@ -76,55 +62,35 @@ namespace osu.Game.Beatmaps
return null;
}
private void fetchRecommendedValues()
private void populateValues()
{
if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId)
if (api.LocalUser.Value.RulesetsStatistics == null)
return;
requestedUserId = api.LocalUser.Value.Id;
// only query API for built-in rulesets
rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
foreach (var kvp in api.LocalUser.Value.RulesetsStatistics)
{
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);
req.Success += result =>
{
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195;
};
api.Queue(req);
});
// algorithm taken from https://github.com/ppy/osu-web/blob/e6e2825516449e3d0f3f5e1852c6bdd3428c3437/app/Models/User.php#L1505
recommendedDifficultyMapping[kvp.Key] = Math.Pow((double)(kvp.Value.PP ?? 0), 0.4) * 0.195;
}
}
/// <returns>
/// Rulesets ordered descending by their respective recommended difficulties.
/// The currently selected ruleset will always be first.
/// </returns>
private IEnumerable<IRulesetInfo> orderedRulesets
private IEnumerable<string> orderedRulesets
{
get
{
if (LoadState < LoadState.Ready || ruleset.Value == null)
return Enumerable.Empty<RulesetInfo>();
return Enumerable.Empty<string>();
return recommendedDifficultyMapping
.OrderByDescending(pair => pair.Value)
.Select(pair => pair.Key)
.Where(r => !r.Equals(ruleset.Value))
.Prepend(ruleset.Value);
.Where(r => !r.Equals(ruleset.Value.ShortName))
.Prepend(ruleset.Value.ShortName);
}
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
switch (state.NewValue)
{
case APIState.Online:
fetchRecommendedValues();
break;
}
});
}
}

View File

@ -85,6 +85,8 @@ namespace osu.Game.Beatmaps.Drawables
downloadTrackers.Add(beatmapDownloadTracker);
AddInternal(beatmapDownloadTracker);
// Note that this is downloading the beatmaps even if they are already downloaded.
// We could rely more on `BeatmapDownloadTracker`'s exposed state to avoid this.
beatmapDownloader.Download(beatmapSet);
}
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected readonly BeatmapDownloadTracker DownloadTracker;
protected BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(HoverSampleSet.Submit)
: base(HoverSampleSet.Button)
{
Expanded = new BindableBool { Disabled = !allowExpansion };

View File

@ -6,7 +6,6 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
@ -245,10 +244,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight)
{

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
@ -226,10 +225,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
});
if (BeatmapSet.HasVideo)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.HasStoryboard)
leftIconArea.Add(new IconPill(FontAwesome.Solid.Image) { IconSize = new Vector2(20) });
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(20) });
if (BeatmapSet.FeaturedInSpotlight)
{

View File

@ -3,14 +3,16 @@
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class IconPill : CircularContainer
public abstract class IconPill : CircularContainer, IHasTooltip
{
public Vector2 IconSize
{
@ -20,7 +22,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly Container iconContainer;
public IconPill(IconUsage icon)
protected IconPill(IconUsage icon)
{
AutoSizeAxes = Axes.Both;
Masking = true;
@ -47,5 +49,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
},
};
}
public abstract LocalisableString TooltipText { get; }
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class StoryboardIconPill : IconPill
{
public StoryboardIconPill()
: base(FontAwesome.Solid.Image)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoStoryboard;
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class VideoIconPill : IconPill
{
public VideoIconPill()
: base(FontAwesome.Solid.Film)
{
}
public override LocalisableString TooltipText => BeatmapsetsStrings.ShowInfoVideo;
}
}

View File

@ -392,7 +392,7 @@ namespace osu.Game.Database
{
total_writes_async.Value++;
using (var realm = getRealmInstance())
await realm.WriteAsync(action);
await realm.WriteAsync(() => action(realm));
}
/// <summary>

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers
/// </summary>
public class ScalingContainer : Container
{
private const float duration = 500;
internal const float TRANSITION_DURATION = 500;
private Bindable<float> sizeX;
private Bindable<float> sizeY;
@ -99,7 +99,7 @@ namespace osu.Game.Graphics.Containers
if (applyUIScale)
{
uiScale = osuConfig.GetBindable<float>(OsuSetting.UIScale);
uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, duration, Easing.OutQuart), true);
uiScale.BindValueChanged(args => this.TransformTo(nameof(CurrentScale), args.NewValue, TRANSITION_DURATION, Easing.OutQuart), true);
}
}
@ -163,10 +163,10 @@ namespace osu.Game.Graphics.Containers
backgroundStack.Push(new ScalingBackgroundScreen());
}
backgroundStack.FadeIn(duration);
backgroundStack.FadeIn(TRANSITION_DURATION);
}
else
backgroundStack?.FadeOut(duration);
backgroundStack?.FadeOut(TRANSITION_DURATION);
}
RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One);
@ -195,13 +195,13 @@ namespace osu.Game.Graphics.Containers
if (requiresMasking)
sizableContainer.Masking = true;
sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart);
sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart);
sizableContainer.MoveTo(targetRect.Location, TRANSITION_DURATION, Easing.OutQuart);
sizableContainer.ResizeTo(targetRect.Size, TRANSITION_DURATION, Easing.OutQuart);
// Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
// Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
// An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None)
sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, TRANSITION_DURATION, requiresMasking ? Easing.OutQuart : Easing.None)
.OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
{
Size = TwoLayerButton.SIZE_EXTENDED;
Child = button = new TwoLayerButton(HoverSampleSet.Submit)
Child = button = new TwoLayerButton
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,

View File

@ -56,8 +56,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly SpriteText spriteText;
private Vector2 hoverSpacing => new Vector2(3f, 0f);
public DialogButton()
: base(HoverSampleSet.Submit)
public DialogButton(HoverSampleSet sampleSet = HoverSampleSet.Button)
: base(sampleSet)
{
RelativeSizeAxes = Axes.X;

View File

@ -36,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface
Icon = FontAwesome.Solid.ExternalLinkAlt,
RelativeSizeAxes = Axes.Both
},
new HoverClickSounds(HoverSampleSet.Submit)
new HoverClickSounds()
};
}

View File

@ -7,6 +7,7 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osuTK.Input;
namespace osu.Game.Graphics.UserInterface
@ -37,7 +38,10 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition))
sampleClick?.Play();
{
sampleClick.Frequency.Value = 0.99 + RNG.NextDouble(0.02);
sampleClick.Play();
}
return base.OnClick(e);
}

View File

@ -10,9 +10,6 @@ namespace osu.Game.Graphics.UserInterface
[Description("default")]
Default,
[Description("submit")]
Submit,
[Description("button")]
Button,

View File

@ -13,6 +13,7 @@ namespace osu.Game.Graphics.UserInterface
{
public class ShearedToggleButton : ShearedButton
{
private Sample? sampleClick;
private Sample? sampleOff;
private Sample? sampleOn;
@ -39,8 +40,9 @@ namespace osu.Game.Graphics.UserInterface
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
sampleClick = audio.Samples.Get(@"UI/default-select");
sampleOn = audio.Samples.Get(@"UI/dropdown-open");
sampleOff = audio.Samples.Get(@"UI/dropdown-close");
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -67,6 +69,8 @@ namespace osu.Game.Graphics.UserInterface
private void playSample()
{
sampleClick?.Play();
if (Active.Value)
sampleOn?.Play();
else

View File

@ -2,11 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@ -65,6 +67,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
get
{
if (BeatmapModelManager.VIDEO_EXTENSIONS.Contains(File.Extension))
return FontAwesome.Regular.FileVideo;
switch (File.Extension)
{
case @".ogg":
@ -77,12 +82,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
case @".png":
return FontAwesome.Regular.FileImage;
case @".mp4":
case @".avi":
case @".mov":
case @".flv":
return FontAwesome.Regular.FileVideo;
default:
return FontAwesome.Regular.File;
}

View File

@ -80,6 +80,7 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight),
new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode),
new KeyBinding(new[] { InputKey.F5 }, GlobalAction.EditorTestGameplay),
new KeyBinding(new[] { InputKey.T }, GlobalAction.EditorTapForBPM),
new KeyBinding(new[] { InputKey.Control, InputKey.H }, GlobalAction.EditorFlipHorizontally),
new KeyBinding(new[] { InputKey.Control, InputKey.J }, GlobalAction.EditorFlipVertically),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.EditorDecreaseDistanceSpacing),
@ -322,5 +323,8 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.DeselectAllMods))]
DeselectAllMods,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorTapForBPM))]
EditorTapForBPM,
}
}

View File

@ -30,12 +30,12 @@ namespace osu.Game.Localisation
public static LocalisableString OutputDevice => new TranslatableString(getKey(@"output_device"), @"Output device");
/// <summary>
/// "Master"
/// "Hitsound stereo separation"
/// </summary>
public static LocalisableString PositionalLevel => new TranslatableString(getKey(@"positional_hitsound_audio_level"), @"Hitsound stereo separation");
/// <summary>
/// "Level"
/// "Master"
/// </summary>
public static LocalisableString MasterVolume => new TranslatableString(getKey(@"master_volume"), @"Master");
@ -69,6 +69,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString OffsetWizard => new TranslatableString(getKey(@"offset_wizard"), @"Offset wizard");
private static string getKey(string key) => $"{prefix}:{key}";
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -174,6 +174,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode");
/// <summary>
/// "Tap for BPM"
/// </summary>
public static LocalisableString EditorTapForBPM => new TranslatableString(getKey(@"editor_tap_for_bpm"), @"Tap for BPM");
/// <summary>
/// "Cycle grid display mode"
/// </summary>
@ -205,7 +210,7 @@ namespace osu.Game.Localisation
public static LocalisableString ToggleInGameInterface => new TranslatableString(getKey(@"toggle_in_game_interface"), @"Toggle in-game interface");
/// <summary>
/// "Toggle Mod Select"
/// "Toggle mod select"
/// </summary>
public static LocalisableString ToggleModSelection => new TranslatableString(getKey(@"toggle_mod_selection"), @"Toggle mod select");
@ -294,6 +299,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ToggleChatFocus => new TranslatableString(getKey(@"toggle_chat_focus"), @"Toggle chat focus");
private static string getKey(string key) => $"{prefix}:{key}";
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -15,10 +15,10 @@ namespace osu.Game.Localisation
public static LocalisableString JoystickGamepad => new TranslatableString(getKey(@"joystick_gamepad"), @"Joystick / Gamepad");
/// <summary>
/// "Deadzone Threshold"
/// "Deadzone"
/// </summary>
public static LocalisableString DeadzoneThreshold => new TranslatableString(getKey(@"deadzone_threshold"), @"Deadzone");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
}

View File

@ -0,0 +1,34 @@
// 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 LayoutSettingsStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.LayoutSettings";
/// <summary>
/// "Checking for fullscreen capabilities..."
/// </summary>
public static LocalisableString CheckingForFullscreenCapabilities => new TranslatableString(getKey(@"checking_for_fullscreen_capabilities"), @"Checking for fullscreen capabilities...");
/// <summary>
/// "osu! is running exclusive fullscreen, guaranteeing low latency!"
/// </summary>
public static LocalisableString OsuIsRunningExclusiveFullscreen => new TranslatableString(getKey(@"osu_is_running_exclusive_fullscreen"), @"osu! is running exclusive fullscreen, guaranteeing low latency!");
/// <summary>
/// "Unable to run exclusive fullscreen. You&#39;ll still experience some input latency."
/// </summary>
public static LocalisableString UnableToRunExclusiveFullscreen => new TranslatableString(getKey(@"unable_to_run_exclusive_fullscreen"), @"Unable to run exclusive fullscreen. You'll still experience some input latency.");
/// <summary>
/// "Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."
/// </summary>
public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -29,6 +29,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DeleteAllBeatmaps => new TranslatableString(getKey(@"delete_all_beatmaps"), @"Delete ALL beatmaps");
/// <summary>
/// "Delete ALL beatmap videos"
/// </summary>
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
/// <summary>
/// "Import scores from stable"
/// </summary>

View File

@ -54,6 +54,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString ExportSkinButton => new TranslatableString(getKey(@"export_skin_button"), @"Export selected skin");
/// <summary>
/// "Delete selected skin"
/// </summary>
public static LocalisableString DeleteSkinButton => new TranslatableString(getKey(@"delete_skin_button"), @"Delete selected skin");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -121,8 +121,16 @@ namespace osu.Game.Online.API
if (isFailing) return;
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
try
{
Logger.Log($@"Performing request {this}", LoggingTarget.Network);
WebRequest.Perform();
}
catch (OperationCanceledException)
{
// ignore this. internally Perform is running async and the fail state may have changed since
// the last check of `isFailing` above.
}
if (isFailing) return;

View File

@ -61,8 +61,7 @@ namespace osu.Game.Online.Chat
/// </summary>
public IBindableList<Channel> AvailableChannels => availableChannels;
[Resolved]
private IAPIProvider api { get; set; }
private readonly IAPIProvider api;
[Resolved]
private UserLookupCache users { get; set; }
@ -71,8 +70,9 @@ namespace osu.Game.Online.Chat
private readonly IBindable<bool> isIdle = new BindableBool();
public ChannelManager()
public ChannelManager(IAPIProvider api)
{
this.api = api;
CurrentChannel.ValueChanged += currentChannelChanged;
}

View File

@ -38,7 +38,6 @@ namespace osu.Game.Online.Chat
}
public DrawableLinkCompiler(IEnumerable<Drawable> parts)
: base(HoverSampleSet.Submit)
{
Parts = parts.ToList();
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Online.Chat
if (!messages.Any())
return;
var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id == messages.First().ChannelId);
var channel = channelManager.JoinedChannels.SingleOrDefault(c => c.Id > 0 && c.Id == messages.First().ChannelId);
if (channel == null)
return;

View File

@ -155,39 +155,42 @@ namespace osu.Game.Online.Chat
{
public Func<Message, ChatLine> CreateChatLineAction;
[Resolved]
private OsuColour colours { get; set; }
public StandAloneDrawableChannel(Channel channel)
: base(channel)
{
}
[BackgroundDependencyLoader]
private void load()
{
ChatLineFlow.Padding = new MarginPadding { Horizontal = 0 };
}
protected override ChatLine CreateChatLine(Message m) => CreateChatLineAction(m);
protected override Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
protected override DaySeparator CreateDaySeparator(DateTimeOffset time) => new StandAloneDaySeparator(time);
}
protected class StandAloneDaySeparator : DaySeparator
{
protected override float TextSize => 14;
protected override float LineHeight => 1;
protected override float Spacing => 5;
protected override float DateAlign => 125;
public StandAloneDaySeparator(DateTimeOffset time)
: base(time)
{
TextSize = 14,
Colour = colours.Yellow,
LineHeight = 1,
Padding = new MarginPadding { Horizontal = 10 },
Margin = new MarginPadding { Vertical = 5 },
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Height = 25;
Colour = colours.Yellow;
}
}
protected class StandAloneMessage : ChatLine
{
protected override float TextSize => 15;
protected override float HorizontalPadding => 10;
protected override float MessagePadding => 120;
protected override float TimestampPadding => 50;
protected override float Spacing => 5;
protected override float TimestampWidth => 45;
protected override float UsernameWidth => 75;
public StandAloneMessage(Message message)
: base(message)

View File

@ -197,6 +197,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == joinedRoom.Settings.PlaylistItemId);
Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser);
@ -737,6 +738,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
APIRoom.AutoStartDuration.Value = Room.Settings.AutoStartDuration;
APIRoom.CurrentPlaylistItem.Value = APIRoom.Playlist.Single(item => item.ID == settings.PlaylistItemId);
RoomUpdated?.Invoke();
}

View File

@ -61,7 +61,13 @@ namespace osu.Game.Online.Rooms
/// Used for serialising to the API.
/// </summary>
[JsonProperty("beatmap_id")]
private int onlineBeatmapId => Beatmap.OnlineID;
private int onlineBeatmapId
{
get => Beatmap.OnlineID;
// This setter is only required for client-side serialise-then-deserialise operations.
// Serialisation is supposed to emit only a `beatmap_id`, but a (non-null) `beatmap` is required on deserialise.
set => Beatmap = new APIBeatmap { OnlineID = value };
}
/// <summary>
/// A beatmap representing this playlist item.

View File

@ -162,6 +162,13 @@ namespace osu.Game.Online.Rooms
Password.BindValueChanged(p => HasPassword.Value = !string.IsNullOrEmpty(p.NewValue));
}
/// <summary>
/// Copies values from another <see cref="Room"/> into this one.
/// </summary>
/// <remarks>
/// **Beware**: This will store references between <see cref="Room"/>s.
/// </remarks>
/// <param name="other">The <see cref="Room"/> to copy values from.</param>
public void CopyFrom(Room other)
{
RoomID.Value = other.RoomID.Value;

View File

@ -39,7 +39,7 @@ namespace osu.Game.Online.Spectator
/// <summary>
/// The states of all users currently being watched.
/// </summary>
public IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
public virtual IBindableDictionary<int, SpectatorState> WatchedUserStates => watchedUserStates;
/// <summary>
/// A global list of all players currently playing.
@ -172,6 +172,7 @@ namespace osu.Game.Online.Spectator
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;
currentState.MaximumScoringValues = state.ScoreProcessor.MaximumScoringValues;
currentBeatmap = state.Beatmap;
currentScore = score;

View File

@ -0,0 +1,192 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
/// <summary>
/// A wrapper over a <see cref="ScoreProcessor"/> for spectated users.
/// This should be used when a local "playable" beatmap is unavailable or expensive to generate for the spectated user.
/// </summary>
public class SpectatorScoreProcessor : Component
{
/// <summary>
/// The current total score.
/// </summary>
public readonly BindableDouble TotalScore = new BindableDouble { MinValue = 0 };
/// <summary>
/// The current accuracy.
/// </summary>
public readonly BindableDouble Accuracy = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
/// <summary>
/// The current combo.
/// </summary>
public readonly BindableInt Combo = new BindableInt();
/// <summary>
/// The <see cref="ScoringMode"/> used to calculate scores.
/// </summary>
public readonly Bindable<ScoringMode> Mode = new Bindable<ScoringMode>();
/// <summary>
/// The applied <see cref="Mod"/>s.
/// </summary>
public IReadOnlyList<Mod> Mods => scoreProcessor?.Mods.Value ?? Array.Empty<Mod>();
private IClock? referenceClock;
/// <summary>
/// The clock used to determine the current score.
/// </summary>
public IClock ReferenceClock
{
get => referenceClock ?? Clock;
set => referenceClock = value;
}
[Resolved]
private SpectatorClient spectatorClient { get; set; } = null!;
[Resolved]
private RulesetStore rulesetStore { get; set; } = null!;
private readonly IBindableDictionary<int, SpectatorState> spectatorStates = new BindableDictionary<int, SpectatorState>();
private readonly List<TimedFrame> replayFrames = new List<TimedFrame>();
private readonly int userId;
private SpectatorState? spectatorState;
private ScoreProcessor? scoreProcessor;
private ScoreInfo? scoreInfo;
public SpectatorScoreProcessor(int userId)
{
this.userId = userId;
}
protected override void LoadComplete()
{
base.LoadComplete();
Mode.BindValueChanged(_ => UpdateScore());
spectatorStates.BindTo(spectatorClient.WatchedUserStates);
spectatorStates.BindCollectionChanged(onSpectatorStatesChanged, true);
spectatorClient.OnNewFrames += onNewFrames;
}
private void onSpectatorStatesChanged(object? sender, NotifyDictionaryChangedEventArgs<int, SpectatorState> e)
{
if (!spectatorStates.TryGetValue(userId, out var userState) || userState.BeatmapID == null || userState.RulesetID == null)
{
scoreProcessor?.RemoveAndDisposeImmediately();
scoreProcessor = null;
scoreInfo = null;
spectatorState = null;
replayFrames.Clear();
return;
}
if (scoreProcessor != null)
return;
Debug.Assert(scoreInfo == null);
RulesetInfo? rulesetInfo = rulesetStore.GetRuleset(userState.RulesetID.Value);
if (rulesetInfo == null)
return;
Ruleset ruleset = rulesetInfo.CreateInstance();
spectatorState = userState;
scoreInfo = new ScoreInfo { Ruleset = rulesetInfo };
scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = userState.Mods.Select(m => m.ToMod(ruleset)).ToArray();
}
private void onNewFrames(int incomingUserId, FrameDataBundle bundle)
{
if (incomingUserId != userId)
return;
Schedule(() =>
{
if (scoreProcessor == null)
return;
replayFrames.Add(new TimedFrame(bundle.Frames.First().Time, bundle.Header));
UpdateScore();
});
}
public void UpdateScore()
{
if (scoreInfo == null || replayFrames.Count == 0)
return;
Debug.Assert(spectatorState != null);
Debug.Assert(scoreProcessor != null);
int frameIndex = replayFrames.BinarySearch(new TimedFrame(ReferenceClock.CurrentTime));
if (frameIndex < 0)
frameIndex = ~frameIndex;
frameIndex = Math.Clamp(frameIndex - 1, 0, replayFrames.Count - 1);
TimedFrame frame = replayFrames[frameIndex];
Debug.Assert(frame.Header != null);
scoreInfo.MaxCombo = frame.Header.MaxCombo;
scoreInfo.Statistics = frame.Header.Statistics;
Accuracy.Value = frame.Header.Accuracy;
Combo.Value = frame.Header.Combo;
scoreProcessor.ExtractScoringValues(frame.Header, out var currentScoringValues, out _);
TotalScore.Value = scoreProcessor.ComputeScore(Mode.Value, currentScoringValues, spectatorState.MaximumScoringValues);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (spectatorClient.IsNotNull())
spectatorClient.OnNewFrames -= onNewFrames;
}
private class TimedFrame : IComparable<TimedFrame>
{
public readonly double Time;
public readonly FrameHeader? Header;
public TimedFrame(double time)
{
Time = time;
}
public TimedFrame(double time, FrameHeader header)
{
Time = time;
Header = header;
}
public int CompareTo(TimedFrame other) => Time.CompareTo(other.Time);
}
}
}

View File

@ -7,6 +7,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using MessagePack;
using osu.Game.Online.API;
using osu.Game.Scoring;
namespace osu.Game.Online.Spectator
{
@ -27,6 +28,9 @@ namespace osu.Game.Online.Spectator
[Key(3)]
public SpectatedUserState State { get; set; }
[Key(4)]
public ScoringValues MaximumScoringValues { get; set; }
public bool Equals(SpectatorState other)
{
if (ReferenceEquals(null, other)) return false;

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -658,11 +659,14 @@ namespace osu.Game
}
protected override IDictionary<FrameworkSetting, object> GetFrameworkConfigDefaults()
=> new Dictionary<FrameworkSetting, object>
{
return new Dictionary<FrameworkSetting, object>
{
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance)
{ FrameworkSetting.WindowMode, WindowMode.Fullscreen }
// General expectation that osu! starts in fullscreen by default (also gives the most predictable performance).
// However, macOS is bound to have issues when using exclusive fullscreen as it takes full control away from OS, therefore borderless is default there.
{ FrameworkSetting.WindowMode, RuntimeInfo.OS == RuntimeInfo.Platform.macOS ? WindowMode.Borderless : WindowMode.Fullscreen }
};
}
protected override void LoadComplete()
{
@ -847,7 +851,7 @@ namespace osu.Game
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true);
loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);

View File

@ -80,6 +80,9 @@ namespace osu.Game
public virtual bool UseDevelopmentServer => DebugUtils.IsDebugBuild;
internal EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary>
@ -268,7 +271,7 @@ namespace osu.Game
dependencies.Cache(SkinManager = new SkinManager(Storage, realm, Host, Resources, Audio, Scheduler));
dependencies.CacheAs<ISkinSource>(SkinManager);
EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
EndpointConfiguration endpoints = CreateEndpoints();
MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl;

View File

@ -5,6 +5,7 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@ -69,7 +70,7 @@ namespace osu.Game.Overlays.AccountCreation
},
usernameTextBox = new OsuTextBox
{
PlaceholderText = UsersStrings.LoginUsername,
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this
},
@ -91,7 +92,7 @@ namespace osu.Game.Overlays.AccountCreation
},
passwordTextBox = new OsuPasswordTextBox
{
PlaceholderText = "password",
PlaceholderText = UsersStrings.LoginPassword.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this,
},

View File

@ -40,7 +40,7 @@ namespace osu.Game.Overlays.BeatmapListing
Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular),
Text = LabelFor(Value)
},
new HoverClickSounds()
new HoverClickSounds(HoverSampleSet.TabSelect)
});
Enabled.Value = true;

View File

@ -26,22 +26,14 @@ namespace osu.Game.Overlays.Chat
{
public class ChatLine : CompositeDrawable
{
public const float LEFT_PADDING = default_message_padding + default_horizontal_padding * 2;
private const float default_message_padding = 200;
protected virtual float MessagePadding => default_message_padding;
private const float default_timestamp_padding = 65;
protected virtual float TimestampPadding => default_timestamp_padding;
private const float default_horizontal_padding = 15;
protected virtual float HorizontalPadding => default_horizontal_padding;
protected virtual float TextSize => 20;
protected virtual float Spacing => 15;
protected virtual float TimestampWidth => 60;
protected virtual float UsernameWidth => 130;
private Color4 usernameColour;
private OsuSpriteText timestamp;
@ -49,7 +41,6 @@ namespace osu.Game.Overlays.Chat
public ChatLine(Message message)
{
Message = message;
Padding = new MarginPadding { Left = HorizontalPadding, Right = HorizontalPadding };
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
@ -90,88 +81,45 @@ namespace osu.Game.Overlays.Chat
? Color4Extensions.FromHex(message.Sender.Colour)
: username_colours[message.Sender.Id % username_colours.Length];
Drawable effectedUsername = username = new OsuSpriteText
InternalChild = new GridContainer
{
Shadow = false,
Colour = senderHasColour ? colours.ChatBlue : usernameColour,
Truncate = true,
EllipsisString = "… :",
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
MaxWidth = MessagePadding - TimestampPadding
};
if (senderHasColour)
{
// Background effect
effectedUsername = new Container
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters
{
Roundness = 1,
Radius = 1,
Colour = Color4.Black.Opacity(0.3f),
Offset = new Vector2(0, 1),
Type = EdgeEffectType.Shadow,
},
Child = new Container
{
AutoSizeAxes = Axes.Both,
Y = 0,
Masking = true,
CornerRadius = 4,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = usernameColour,
},
new Container
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
Child = username
}
}
}
};
}
InternalChildren = new Drawable[]
{
new Container
{
Size = new Vector2(MessagePadding, TextSize),
Children = new Drawable[]
{
timestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true)
},
new MessageSender(message.Sender)
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = effectedUsername,
},
}
new Dimension(GridSizeMode.Absolute, TimestampWidth + Spacing + UsernameWidth + Spacing),
new Dimension(),
},
new Container
Content = new[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = MessagePadding + HorizontalPadding },
Children = new Drawable[]
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
timestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: TextSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
MaxWidth = TimestampWidth,
},
new MessageSender(message.Sender)
{
Width = UsernameWidth,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Child = createUsername(),
Margin = new MarginPadding { Horizontal = Spacing },
},
},
},
ContentFlow = new LinkFlowContainer(t =>
{
t.Shadow = false;
@ -190,7 +138,7 @@ namespace osu.Game.Overlays.Chat
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
}
}
},
}
};
}
@ -233,7 +181,7 @@ namespace osu.Game.Overlays.Chat
timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}";
username.Text = $@"{message.Sender.Username}" + (senderHasColour || message.IsAction ? "" : ":");
username.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list
message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chatManager?.AvailableChannels.Any(c => c.Name == link.Argument.ToString()) != true);
@ -242,6 +190,62 @@ namespace osu.Game.Overlays.Chat
ContentFlow.AddLinks(message.DisplayContent, message.Links);
}
private Drawable createUsername()
{
username = new OsuSpriteText
{
Shadow = false,
Colour = senderHasColour ? colours.ChatBlue : usernameColour,
Truncate = true,
EllipsisString = "…",
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true),
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
MaxWidth = UsernameWidth,
};
if (!senderHasColour)
return username;
// Background effect
return new Container
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
EdgeEffect = new EdgeEffectParameters
{
Roundness = 1,
Radius = 1,
Colour = Color4.Black.Opacity(0.3f),
Offset = new Vector2(0, 1),
Type = EdgeEffectType.Shadow,
},
Child = new Container
{
AutoSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 4,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = usernameColour,
},
new Container
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 },
Child = username
}
}
}
};
}
private class MessageSender : OsuClickableContainer, IHasContextMenu
{
private readonly APIUser sender;

View File

@ -1,109 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.Chat
{
public class ChatOverlayDrawableChannel : DrawableChannel
{
public ChatOverlayDrawableChannel(Channel channel)
: base(channel)
{
}
[BackgroundDependencyLoader]
private void load()
{
ChatLineFlow.Padding = new MarginPadding(0);
}
protected override Drawable CreateDaySeparator(DateTimeOffset time) => new ChatOverlayDaySeparator(time);
private class ChatOverlayDaySeparator : Container
{
private readonly DateTimeOffset time;
public ChatOverlayDaySeparator(DateTimeOffset time)
{
this.time = time;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = 15, Vertical = 20 };
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
new Dimension(GridSizeMode.Absolute, 15),
new Dimension(),
},
Content = new[]
{
new[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 15),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
new Circle
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.X,
Height = 2,
},
Drawable.Empty(),
new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
Font = OsuFont.Torus.With(size: 15, weight: FontWeight.SemiBold),
Colour = colourProvider.Content1,
},
},
},
},
Drawable.Empty(),
new Circle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.X,
Height = 2,
},
},
},
};
}
}
}
}

View File

@ -0,0 +1,105 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Overlays.Chat
{
public class DaySeparator : Container
{
protected virtual float TextSize => 15;
protected virtual float LineHeight => 2;
protected virtual float DateAlign => 205;
protected virtual float Spacing => 15;
private readonly DateTimeOffset time;
[Resolved(CanBeNull = true)]
private OverlayColourProvider? colourProvider { get; set; }
public DaySeparator(DateTimeOffset time)
{
this.time = time;
Height = 40;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RowDimensions = new[] { new Dimension() },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, DateAlign),
new Dimension(GridSizeMode.Absolute, Spacing),
new Dimension(),
},
Content = new[]
{
new[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[] { new Dimension() },
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, Spacing),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = LineHeight,
Colour = colourProvider?.Background5 ?? Colour4.White,
},
Drawable.Empty(),
new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Text = time.ToLocalTime().ToString("dd MMMM yyyy").ToUpper(),
Font = OsuFont.Torus.With(size: TextSize, weight: FontWeight.SemiBold),
Colour = colourProvider?.Content1 ?? Colour4.White,
},
}
},
},
Drawable.Empty(),
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = LineHeight,
Colour = colourProvider?.Background5 ?? Colour4.White,
},
}
}
};
}
}
}

View File

@ -7,14 +7,9 @@ using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osuTK.Graphics;
@ -40,9 +35,6 @@ namespace osu.Game.Overlays.Chat
}
}
[Resolved]
private OsuColour colours { get; set; }
public DrawableChannel(Channel channel)
{
Channel = channel;
@ -67,7 +59,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Left = 20, Right = 20 },
Padding = new MarginPadding { Horizontal = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
@ -121,11 +113,7 @@ namespace osu.Game.Overlays.Chat
protected virtual ChatLine CreateChatLine(Message m) => new ChatLine(m);
protected virtual Drawable CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time)
{
Colour = colours.ChatBlue.Lighten(0.7f),
Margin = new MarginPadding { Vertical = 10 },
};
protected virtual DaySeparator CreateDaySeparator(DateTimeOffset time) => new DaySeparator(time);
private void newMessagesArrived(IEnumerable<Message> newMessages) => Schedule(() =>
{
@ -203,69 +191,5 @@ namespace osu.Game.Overlays.Chat
});
private IEnumerable<ChatLine> chatLines => ChatLineFlow.Children.OfType<ChatLine>();
public class DaySeparator : Container
{
public float TextSize
{
get => text.Font.Size;
set => text.Font = text.Font.With(size: value);
}
private float lineHeight = 2;
public float LineHeight
{
get => lineHeight;
set => lineHeight = leftBox.Height = rightBox.Height = value;
}
private readonly SpriteText text;
private readonly Box leftBox;
private readonly Box rightBox;
public DaySeparator(DateTimeOffset time)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Child = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), },
Content = new[]
{
new Drawable[]
{
leftBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = lineHeight,
},
text = new OsuSpriteText
{
Margin = new MarginPadding { Horizontal = 10 },
Text = time.ToLocalTime().ToString("dd MMM yyyy"),
},
rightBox = new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = lineHeight,
},
}
}
};
}
}
}
}

View File

@ -6,6 +6,8 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -16,6 +18,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osuTK;
@ -32,6 +35,8 @@ namespace osu.Game.Overlays.Chat.Listing
public IEnumerable<LocalisableString> FilterTerms => new LocalisableString[] { Channel.Name, Channel.Topic ?? string.Empty };
public bool MatchingFilter { set => this.FadeTo(value ? 1f : 0f, 100); }
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
private Box hoverBox = null!;
private SpriteIcon checkbox = null!;
private OsuSpriteText channelText = null!;
@ -46,14 +51,20 @@ namespace osu.Game.Overlays.Chat.Listing
private const float vertical_margin = 1.5f;
private Sample? sampleJoin;
private Sample? sampleLeave;
public ChannelListingItem(Channel channel)
{
Channel = channel;
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
sampleJoin = audio.Samples.Get(@"UI/check-on");
sampleLeave = audio.Samples.Get(@"UI/check-off");
Masking = true;
CornerRadius = 5;
RelativeSizeAxes = Axes.X;
@ -156,7 +167,19 @@ namespace osu.Game.Overlays.Chat.Listing
}
}, true);
Action = () => (channelJoined.Value ? OnRequestLeave : OnRequestJoin)?.Invoke(Channel);
Action = () =>
{
if (channelJoined.Value)
{
OnRequestLeave?.Invoke(Channel);
sampleLeave?.Play();
}
else
{
OnRequestJoin?.Invoke(Channel);
sampleJoin?.Play();
}
};
}
protected override bool OnHover(HoverEvent e)

View File

@ -38,9 +38,9 @@ namespace osu.Game.Overlays
private LoadingLayer loading = null!;
private ChannelListing channelListing = null!;
private ChatTextBar textBar = null!;
private Container<ChatOverlayDrawableChannel> currentChannelContainer = null!;
private Container<DrawableChannel> currentChannelContainer = null!;
private readonly Dictionary<Channel, ChatOverlayDrawableChannel> loadedChannels = new Dictionary<Channel, ChatOverlayDrawableChannel>();
private readonly Dictionary<Channel, DrawableChannel> loadedChannels = new Dictionary<Channel, DrawableChannel>();
protected IEnumerable<DrawableChannel> DrawableChannels => loadedChannels.Values;
@ -126,7 +126,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
currentChannelContainer = new Container<ChatOverlayDrawableChannel>
currentChannelContainer = new Container<DrawableChannel>
{
RelativeSizeAxes = Axes.Both,
},
@ -313,7 +313,7 @@ namespace osu.Game.Overlays
loading.Show();
// Ensure the drawable channel is stored before async load to prevent double loading
ChatOverlayDrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
DrawableChannel drawableChannel = CreateDrawableChannel(newChannel);
loadedChannels.Add(newChannel, drawableChannel);
LoadComponentAsync(drawableChannel, loadedDrawable =>
@ -338,7 +338,7 @@ namespace osu.Game.Overlays
channelManager.MarkChannelAsRead(newChannel);
}
protected virtual ChatOverlayDrawableChannel CreateDrawableChannel(Channel newChannel) => new ChatOverlayDrawableChannel(newChannel);
protected virtual DrawableChannel CreateDrawableChannel(Channel newChannel) => new DrawableChannel(newChannel);
private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
{
@ -361,7 +361,7 @@ namespace osu.Game.Overlays
if (loadedChannels.ContainsKey(channel))
{
ChatOverlayDrawableChannel loaded = loadedChannels[channel];
DrawableChannel loaded = loadedChannels[channel];
loadedChannels.Remove(channel);
// DrawableChannel removed from cache must be manually disposed
loaded.Dispose();

View File

@ -154,12 +154,15 @@ namespace osu.Game.Overlays.FirstRunSetup
var downloadTracker = tutorialDownloader.DownloadTrackers.First();
downloadTracker.State.BindValueChanged(state =>
{
if (state.NewValue == DownloadState.LocallyAvailable)
downloadTutorialButton.Complete();
}, true);
downloadTracker.Progress.BindValueChanged(progress =>
{
downloadTutorialButton.SetProgress(progress.NewValue, false);
if (progress.NewValue == 1)
downloadTutorialButton.Complete();
}, true);
}

View File

@ -126,6 +126,7 @@ namespace osu.Game.Overlays.FirstRunSetup
private class SampleScreenContainer : CompositeDrawable
{
private readonly OsuScreen screen;
// Minimal isolation from main game.
[Cached]
@ -151,6 +152,9 @@ namespace osu.Game.Overlays.FirstRunSetup
RelativeSizeAxes = Axes.Both;
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
new DependencyContainer(new DependencyIsolationContainer(base.CreateChildDependencies(parent)));
[BackgroundDependencyLoader]
private void load(AudioManager audio, TextureStore textures, RulesetStore rulesets)
{
@ -197,5 +201,41 @@ namespace osu.Game.Overlays.FirstRunSetup
stack.PushSynchronously(screen);
}
}
private class DependencyIsolationContainer : IReadOnlyDependencyContainer
{
private readonly IReadOnlyDependencyContainer parentDependencies;
private readonly Type[] isolatedTypes =
{
typeof(OsuGame)
};
public DependencyIsolationContainer(IReadOnlyDependencyContainer parentDependencies)
{
this.parentDependencies = parentDependencies;
}
public object Get(Type type)
{
if (isolatedTypes.Contains(type))
return null;
return parentDependencies.Get(type);
}
public object Get(Type type, CacheInfo info)
{
if (isolatedTypes.Contains(type))
return null;
return parentDependencies.Get(type, info);
}
public void Inject<T>(T instance) where T : class
{
parentDependencies.Inject(instance);
}
}
}
}

View File

@ -14,7 +14,6 @@ using osu.Framework.Platform;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.News
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.News
private TextFlowContainer main;
public NewsCard(APINewsPost post)
: base(HoverSampleSet.Submit)
{
this.post = post;

View File

@ -18,7 +18,6 @@ using System.Diagnostics;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Platform;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.News.Sidebar
{
@ -129,7 +128,6 @@ namespace osu.Game.Overlays.News.Sidebar
private readonly APINewsPost post;
public PostButton(APINewsPost post)
: base(HoverSampleSet.Submit)
{
this.post = post;

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private Sample sampleOpen;
private Sample sampleClose;
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverClickSounds();
public ExpandDetailsButton()
{

View File

@ -6,7 +6,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Profile.Sections
{
@ -18,7 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections
private readonly IBeatmapInfo beatmapInfo;
protected BeatmapMetadataContainer(IBeatmapInfo beatmapInfo)
: base(HoverSampleSet.Submit)
{
this.beatmapInfo = beatmapInfo;

View File

@ -4,6 +4,7 @@
using System;
using System.Drawing;
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
@ -13,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Platform.Windows;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@ -34,10 +36,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private Bindable<Size> sizeFullscreen;
private readonly BindableList<Size> resolutions = new BindableList<Size>(new[] { new Size(9999, 9999) });
private readonly IBindable<FullscreenCapability> fullscreenCapability = new Bindable<FullscreenCapability>(FullscreenCapability.Capable);
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private GameHost host { get; set; }
private SettingsDropdown<Size> resolutionDropdown;
private SettingsDropdown<Display> displayDropdown;
private SettingsDropdown<WindowMode> windowModeDropdown;
@ -65,6 +71,9 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModes.BindTo(host.Window.SupportedWindowModes);
}
if (host.Window is WindowsWindow windowsWindow)
fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
Children = new Drawable[]
{
windowModeDropdown = new SettingsDropdown<WindowMode>
@ -139,6 +148,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
},
};
fullscreenCapability.BindValueChanged(_ => Schedule(updateScreenModeWarning), true);
}
protected override void LoadComplete()
@ -150,8 +161,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModeDropdown.Current.BindValueChanged(mode =>
{
updateDisplayModeDropdowns();
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? GraphicsSettingsStrings.NotFullscreenNote : default;
updateScreenModeWarning();
}, true);
windowModes.BindCollectionChanged((sender, args) =>
@ -213,6 +223,48 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
private void updateScreenModeWarning()
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.macOS)
{
if (windowModeDropdown.Current.Value == WindowMode.Fullscreen)
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.FullscreenMacOSNote, true);
else
windowModeDropdown.ClearNoticeText();
return;
}
if (windowModeDropdown.Current.Value != WindowMode.Fullscreen)
{
windowModeDropdown.SetNoticeText(GraphicsSettingsStrings.NotFullscreenNote, true);
return;
}
if (host.Window is WindowsWindow)
{
switch (fullscreenCapability.Value)
{
case FullscreenCapability.Unknown:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.CheckingForFullscreenCapabilities, true);
break;
case FullscreenCapability.Capable:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.OsuIsRunningExclusiveFullscreen);
break;
case FullscreenCapability.Incapable:
windowModeDropdown.SetNoticeText(LayoutSettingsStrings.UnableToRunExclusiveFullscreen, true);
break;
}
}
else
{
// We can only detect exclusive fullscreen status on windows currently.
windowModeDropdown.ClearNoticeText();
}
}
private void bindPreviewEvent(Bindable<float> bindable)
{
bindable.ValueChanged += _ =>

View File

@ -48,7 +48,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
frameLimiterDropdown.Current.BindValueChanged(limit =>
{
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? GraphicsSettingsStrings.UnlimitedFramesNote : default;
switch (limit.NewValue)
{
case FrameSync.Unlimited:
frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true);
break;
default:
frameLimiterDropdown.ClearNoticeText();
break;
}
}, true);
}
}

View File

@ -117,9 +117,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
{
if (highPrecision.NewValue)
highPrecisionMouse.WarningText = MouseSettingsStrings.HighPrecisionPlatformWarning;
highPrecisionMouse.SetNoticeText(MouseSettingsStrings.HighPrecisionPlatformWarning, true);
else
highPrecisionMouse.WarningText = null;
highPrecisionMouse.ClearNoticeText();
}
}, true);
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation;
@ -95,11 +96,13 @@ namespace osu.Game.Overlays.Settings.Sections.Input
Origin = Anchor.TopCentre,
Text = TabletSettingsStrings.NoTabletDetected,
},
new SettingsNoticeText(colours)
new LinkFlowContainer(cp => cp.Colour = colours.Yellow)
{
TextAnchor = Anchor.TopCentre,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}.With(t =>
{
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)

View File

@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private SettingsButton deleteSkinsButton;
private SettingsButton restoreButton;
private SettingsButton undeleteButton;
private SettingsButton deleteBeatmapVideosButton;
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, [CanBeNull] LegacyImportManager legacyImportManager, IDialogOverlay dialogOverlay)
@ -58,6 +59,19 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
}
});
Add(deleteBeatmapVideosButton = new DangerousSettingsButton
{
Text = MaintenanceSettingsStrings.DeleteAllBeatmapVideos,
Action = () =>
{
dialogOverlay?.Push(new MassVideoDeleteConfirmationDialog(() =>
{
deleteBeatmapVideosButton.Enabled.Value = false;
Task.Run(beatmaps.DeleteAllVideos).ContinueWith(t => Schedule(() => deleteBeatmapVideosButton.Enabled.Value = true));
}));
}
});
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importScoresButton = new SettingsButton

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class MassVideoDeleteConfirmationDialog : MassDeleteConfirmationDialog
{
public MassVideoDeleteConfirmationDialog(Action deleteAction)
: base(deleteAction)
{
BodyText = "All beatmap videos? This cannot be undone!";
}
}
}

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osu.Game.Skinning.Editor;
using Realms;
@ -67,6 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections
Action = () => skinEditor?.ToggleVisibility(),
},
new ExportSkinButton(),
new DeleteSkinButton(),
};
config.BindWith(OsuSetting.Skin, configBindable);
@ -202,5 +204,36 @@ namespace osu.Game.Overlays.Settings.Sections
}
}
}
public class DeleteSkinButton : DangerousSettingsButton
{
[Resolved]
private SkinManager skins { get; set; }
[Resolved(CanBeNull = true)]
private IDialogOverlay dialogOverlay { get; set; }
private Bindable<Skin> currentSkin;
[BackgroundDependencyLoader]
private void load()
{
Text = SkinSettingsStrings.DeleteSkinButton;
Action = delete;
}
protected override void LoadComplete()
{
base.LoadComplete();
currentSkin = skins.CurrentSkin.GetBoundCopy();
currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.PerformRead(s => !s.Protected), true);
}
private void delete()
{
dialogOverlay?.Push(new SkinDeleteDialog(currentSkin.Value));
}
}
}
}

View File

@ -61,7 +61,10 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
user.BindValueChanged(u =>
{
backgroundSourceDropdown.WarningText = u.NewValue?.IsSupporter != true ? UserInterfaceStrings.NotSupporterNote : default;
if (u.NewValue?.IsSupporter != true)
backgroundSourceDropdown.SetNoticeText(UserInterfaceStrings.NotSupporterNote, true);
else
backgroundSourceDropdown.ClearNoticeText();
}, true);
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText;
private OsuTextFlowContainer warningText;
private OsuTextFlowContainer noticeText;
public bool ShowsDefaultIndicator = true;
private readonly Container defaultValueIndicatorContainer;
@ -70,27 +70,32 @@ namespace osu.Game.Overlays.Settings
}
/// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// Clear any warning text.
/// </summary>
public LocalisableString? WarningText
public void ClearNoticeText()
{
set
noticeText?.Expire();
noticeText = null;
}
/// <summary>
/// Set the text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to provide feedback to a user about a sub-optimal setting.
/// </summary>
/// <param name="text">The text to display.</param>
/// <param name="isWarning">Whether the text is in a warning state. Will decide how this is visually represented.</param>
public void SetNoticeText(LocalisableString text, bool isWarning = false)
{
ClearNoticeText();
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(noticeText = new LinkFlowContainer(cp => cp.Colour = isWarning ? colours.Yellow : colours.Green)
{
bool hasValue = value != default;
if (warningText == null)
{
if (!hasValue)
return;
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(warningText = new SettingsNoticeText(colours) { Margin = new MarginPadding { Bottom = 5 } });
}
warningText.Alpha = hasValue ? 1 : 0;
warningText.Text = value ?? default;
}
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 5 },
Text = text,
});
}
public virtual Bindable<T> Current

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings
{
public class SettingsNoticeText : LinkFlowContainer
{
public SettingsNoticeText(OsuColour colours)
: base(s => s.Colour = colours.Yellow)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
}
}
}

View File

@ -20,7 +20,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Graphics;
@ -83,7 +82,6 @@ namespace osu.Game.Overlays.Toolbar
private RealmAccess realm { get; set; }
protected ToolbarButton()
: base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;

View File

@ -11,7 +11,6 @@ using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
@ -29,7 +28,6 @@ namespace osu.Game.Overlays.Toolbar
private AnalogClockDisplay analog;
public ToolbarClock()
: base(HoverSampleSet.Toolbar)
{
RelativeSizeAxes = Axes.Y;
AutoSizeAxes = Axes.X;

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
@ -28,7 +27,7 @@ using osuTK.Graphics;
namespace osu.Game.Overlays.Volume
{
public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction>, IStateful<SelectionState>
public class VolumeMeter : Container, IStateful<SelectionState>
{
private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow;
@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Volume
[BackgroundDependencyLoader]
private void load(OsuColour colours, AudioManager audio)
{
hoverSample = audio.Samples.Get($"UI/{HoverSampleSet.Button.GetDescription()}-hover");
hoverSample = audio.Samples.Get($@"UI/{HoverSampleSet.Button.GetDescription()}-hover");
notchSample = audio.Samples.Get(@"UI/notch-tick");
sampleLastPlaybackTime = Time.Current;
@ -132,7 +131,7 @@ namespace osu.Game.Overlays.Volume
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Progress under covers for smoothing",
Name = @"Progress under covers for smoothing",
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Child = volumeCircle = new CircularProgress
@ -144,7 +143,7 @@ namespace osu.Game.Overlays.Volume
},
new Circle
{
Name = "Inner Cover",
Name = @"Inner Cover",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
@ -153,7 +152,7 @@ namespace osu.Game.Overlays.Volume
},
new Container
{
Name = "Progress overlay for glow",
Name = @"Progress overlay for glow",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
@ -365,27 +364,6 @@ namespace osu.Game.Overlays.Volume
{
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (!IsHovered)
return false;
switch (e.Action)
{
case GlobalAction.SelectPreviousGroup:
State = SelectionState.Selected;
adjust(1, false);
return true;
case GlobalAction.SelectNextGroup:
State = SelectionState.Selected;
adjust(-1, false);
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}

View File

@ -17,6 +17,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays.Volume;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Overlays
{
@ -171,6 +172,30 @@ namespace osu.Game.Overlays
return base.OnMouseMove(e);
}
protected override bool OnKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.Left:
Adjust(GlobalAction.PreviousVolumeMeter);
return true;
case Key.Right:
Adjust(GlobalAction.NextVolumeMeter);
return true;
case Key.Down:
Adjust(GlobalAction.DecreaseVolume);
return true;
case Key.Up:
Adjust(GlobalAction.IncreaseVolume);
return true;
}
return base.OnKeyDown(e);
}
protected override bool OnHover(HoverEvent e)
{
schedulePopOut();

View File

@ -117,9 +117,8 @@ namespace osu.Game.Rulesets.Scoring
/// <remarks>
/// If the provided replay frame does not have any header information, this will be a noop.
/// </remarks>
/// <param name="ruleset">The ruleset to be used for retrieving statistics.</param>
/// <param name="frame">The replay frame to read header statistics from.</param>
public virtual void ResetFromReplayFrame(Ruleset ruleset, ReplayFrame frame)
public virtual void ResetFromReplayFrame(ReplayFrame frame)
{
if (frame.Header == null)
return;

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