1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-08 16:24:19 +08:00

Compare commits

...

101 Commits

188 changed files with 2553 additions and 1099 deletions
@@ -0,0 +1,19 @@
name: Copy labels from linked issues
on:
pull_request:
types: [opened]
permissions:
issues: write # to read the labels of any linked issue(s), and to put the found labels if any on the PR
# not granting any `pull_requests` permissions because in github's modeling pull requests are a subset of issues. it's confusing.
jobs:
copy-labels:
runs-on: ubuntu-latest
name: Copy labels from linked issues
steps:
- name: Copy labels
uses: michalvankodev/copy-issue-labels@v1.3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.108.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2026.129.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
+7 -3
View File
@@ -146,9 +146,13 @@ namespace osu.Desktop
{
base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
// Apple operating systems use a better icon provided via external assets.
if (!RuntimeInfo.IsApple)
{
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
host.Window.SetIconFromStream(iconStream);
}
host.Window.Title = Name;
}
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
+1 -1
View File
@@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Mania.UI
var hitWindows = new ManiaHitWindows();
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(hitWindows.IsHitResultAllowed)));
RegisterPool<BarLine, DrawableBarLine>(50, 200);
}
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
@@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Debug.Assert(drawableHitObject.HitObject.HitWindows != null);
double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current;
scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay));
scheduledTasks.Add(Scheduler.AddDelayed(drawableHitObject.TriggerJudgement, delay));
return drawableHitObject;
}
+9 -1
View File
@@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods
[SettingSource(
"Max size at combo",
"The combo count at which the cursor reaches its maximum size",
SettingControlType = typeof(SettingsSlider<int, RoundedSliderBar<int>>)
SettingControlType = typeof(SettingsSlider<int, MaxSizeComboSlider>)
)]
public BindableInt MaxSizeComboCount { get; } = new BindableInt(50)
{
@@ -85,4 +85,12 @@ namespace osu.Game.Rulesets.Osu.Mods
cursor.ModScaleAdjust.Value = (float)Interpolation.Lerp(cursor.ModScaleAdjust.Value, currentSize, Math.Clamp(cursor.Time.Elapsed / TRANSITION_DURATION, 0, 1));
}
}
public partial class MaxSizeComboSlider : RoundedSliderBar<int>
{
public MaxSizeComboSlider()
{
KeyboardStep = 1;
}
}
}
@@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@@ -71,6 +73,8 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModTargetPractice)).ToArray();
protected override void ApplySettings(BeatmapDifficulty difficulty)
{
base.ApplySettings(difficulty);
@@ -27,8 +27,10 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => "Burn the notes into your memory.";
//Alters the transforms of the approach circles, breaking the effects of these mods.
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth) }).ToArray();
/// <remarks>
/// Incompatible with all mods that directly modify or indirectly depend on <see cref="OsuHitObject.TimePreempt"/>, or alter the behaviour of approach circles.
/// </remarks>
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth), typeof(OsuModHidden) }).ToArray();
public override ModType Type => ModType.Fun;
+1 -1
View File
@@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override LocalisableString Description => @"Play with no approach circles and fading circles/sliders.";
public override double ScoreMultiplier => UsesDefaultConfiguration ? 1.06 : 1;
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth) };
public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles), typeof(OsuModSpinIn), typeof(OsuModDepth), typeof(OsuModFreezeFrame) };
public const double FADE_IN_DURATION_MULTIPLIER = 0.4;
public const double FADE_OUT_DURATION_MULTIPLIER = 0.3;
@@ -48,7 +48,8 @@ namespace osu.Game.Rulesets.Osu.Mods
typeof(OsuModSpunOut),
typeof(OsuModStrictTracking),
typeof(OsuModSuddenDeath),
typeof(OsuModDepth)
typeof(OsuModDepth),
typeof(OsuModDifficultyAdjust),
}).ToArray();
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
@@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Name => "Traceable";
public override string Acronym => "TC";
public override IconUsage? Icon => OsuIcon.ModTraceable;
public override ModType Type => ModType.Fun;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Put your faith in the approach circles...";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
+1 -2
View File
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModHardRock(),
new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()),
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModHidden(), new OsuModTraceable()),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
@@ -209,7 +209,6 @@ namespace osu.Game.Rulesets.Osu
new OsuModSpinIn(),
new MultiMod(new OsuModGrow(), new OsuModDeflate()),
new MultiMod(new ModWindUp(), new ModWindDown()),
new OsuModTraceable(),
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
+1 -3
View File
@@ -35,11 +35,9 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
</plist>
+1 -1
View File
@@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
HitResult[] usableHitResults = Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray();
HitResult[] usableHitResults = Enum.GetValues<HitResult>().Where(hitWindows.IsHitResultAllowed).ToArray();
AddInternal(judgementPooler = new JudgementPooler<DrawableTaikoJudgement>(usableHitResults));
+25 -2
View File
@@ -35,8 +35,31 @@
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CFBundleIcons~ipad</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
</array>
<key>CFBundleIconName</key>
<string>AppIcon</string>
</dict>
</dict>
<key>CFBundleIcons</key>
<dict>
<key>CFBundlePrimaryIcon</key>
<dict>
<key>CFBundleIconFiles</key>
<array>
<string>AppIcon60x60</string>
<string>AppIcon76x76</string>
</array>
<key>CFBundleIconName</key>
<string>AppIcon</string>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -15,6 +16,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Visual;
@@ -34,6 +36,12 @@ namespace osu.Game.Tests.Beatmaps
private IBindable<StarDifficulty> starDifficultyBindable;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private BeatmapDifficultyCache actualDifficultyCache { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGameBase osu)
{
@@ -55,6 +63,36 @@ namespace osu.Game.Tests.Beatmaps
AddUntilStep($"star difficulty -> {BASE_STARS}", () => starDifficultyBindable.Value.Stars == BASE_STARS);
}
[Test]
public void TestInvalidationFlow()
{
BeatmapInfo postEditBeatmapInfo = null;
BeatmapInfo preEditBeatmapInfo = null;
IBindable<StarDifficulty> bindableDifficulty = null;
AddStep("get bindable stars", () =>
{
preEditBeatmapInfo = importedSet.Beatmaps.First();
bindableDifficulty = actualDifficultyCache.GetBindableDifficulty(preEditBeatmapInfo);
});
AddUntilStep("wait for stars retrieved", () => bindableDifficulty.Value.Stars, () => Is.GreaterThan(0));
AddStep("remove all hitobjects", () =>
{
var working = beatmapManager.GetWorkingBeatmap(preEditBeatmapInfo);
((IList<HitObject>)working.Beatmap.HitObjects).Clear();
beatmapManager.Save(working.BeatmapInfo, working.Beatmap);
postEditBeatmapInfo = working.BeatmapInfo;
});
AddAssert("stars is now zero", () => actualDifficultyCache.GetDifficultyAsync(postEditBeatmapInfo).GetResultSafely()!.Value.Stars, () => Is.Zero);
AddUntilStep("bindable stars is now zero", () => bindableDifficulty.Value.Stars, () => Is.Zero);
}
[Test]
public void TestStarDifficultyChangesOnModSettings()
{
@@ -76,6 +114,30 @@ namespace osu.Game.Tests.Beatmaps
AddUntilStep($"star difficulty -> {BASE_STARS + 1.75}", () => starDifficultyBindable.Value.Stars == BASE_STARS + 1.75);
}
[Test]
public void TestStarDifficultyChangesOnModSettingsCorrectlyTrackAcrossReferenceChanges()
{
OsuModDoubleTime dt = null;
AddStep("set computation function", () => difficultyCache.ComputeDifficulty = lookup =>
{
var modRateAdjust = (ModRateAdjust)lookup.OrderedMods.SingleOrDefault(mod => mod is ModRateAdjust);
return new StarDifficulty(BASE_STARS + modRateAdjust?.SpeedChange.Value ?? 0, 0);
});
AddStep("change selected mod to DT", () => SelectedMods.Value = new[] { dt = new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.5}", () => starDifficultyBindable.Value.Stars == BASE_STARS + 1.5);
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 1.25);
AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value.Stars == BASE_STARS + 1.25);
AddStep("reconstruct DT mod with same settings", () => SelectedMods.Value = new[] { dt = (OsuModDoubleTime)dt.DeepClone() });
AddUntilStep($"star difficulty -> {BASE_STARS + 1.25}", () => starDifficultyBindable.Value.Stars == BASE_STARS + 1.25);
AddStep("change DT speed to 1.25", () => dt.SpeedChange.Value = 2);
AddUntilStep($"star difficulty -> {BASE_STARS + 2}", () => starDifficultyBindable.Value.Stars == BASE_STARS + 2);
}
[Test]
public void TestStarDifficultyAdjustHashCodeConflict()
{
@@ -122,8 +184,10 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestKeyDoesntEqualWithDifferentModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.1 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.9 } } });
Assert.That(key1, Is.Not.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.Not.EqualTo(key2.GetHashCode()));
@@ -132,8 +196,10 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestKeyEqualWithMatchingModSettings()
{
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 }, new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = guid }, new RulesetInfo { OnlineID = 0 },
new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
Assert.That(key1, Is.EqualTo(key2));
Assert.That(key1.GetHashCode(), Is.EqualTo(key2.GetHashCode()));
@@ -247,7 +247,7 @@ namespace osu.Game.Tests.Beatmaps
AddStep("change all start times", () =>
{
editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
editorBeatmap.HitObjectUpdated += updatedObjects.Add;
for (int i = 0; i < 10; i++)
allHitObjects[i].StartTime += 10;
@@ -282,7 +282,7 @@ namespace osu.Game.Tests.Beatmaps
AddStep("change start time twice", () =>
{
editorBeatmap.HitObjectUpdated += h => updatedObjects.Add(h);
editorBeatmap.HitObjectUpdated += updatedObjects.Add;
editorBeatmap.HitObjects[0].StartTime = 10;
editorBeatmap.HitObjects[0].StartTime = 20;
@@ -104,10 +104,7 @@ namespace osu.Game.Tests.Database
Assert.AreNotEqual(detachedBeatmapSet.Status, BeatmapOnlineStatus.Ranked);
detachedBeatmapSet.Status = BeatmapOnlineStatus.Ranked;
beatmapSet.PerformWrite(s =>
{
detachedBeatmapSet.CopyChangesToRealm(s);
});
beatmapSet.PerformWrite(detachedBeatmapSet.CopyChangesToRealm);
beatmapSet.PerformRead(s =>
{
@@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
List<SkinBlueprint> blueprints = new List<SkinBlueprint>();
AddStep("clear list", () => blueprints.Clear());
AddStep("clear list", blueprints.Clear);
for (int i = 0; i < 3; i++)
{
@@ -3,18 +3,16 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Overlays.Dashboard.CurrentlyOnline;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
@@ -23,6 +21,26 @@ namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneCurrentlyOnlineDisplay : OsuTestScene
{
private static readonly string[] usernames =
{
"fieryrage",
"Kerensa",
"MillhioreF",
"Player01",
"smoogipoo",
"Ephemeral",
"BTMC",
"Cilvery",
"m980",
"HappyStick",
"LittleEndu",
"frenzibyte",
"Zallius",
"BanchoBot",
"rocketminer210",
"pishifat"
};
private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
private TestSpectatorClient spectatorClient = null!;
@@ -36,11 +54,34 @@ namespace osu.Game.Tests.Visual.Online
{
spectatorClient = new TestSpectatorClient();
metadataClient = new TestMetadataClient();
var lookupCache = new TestUserLookupCache();
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case LookupUsersRequest lookupUsersRequest:
var users = lookupUsersRequest.UserIds.Select(id =>
{
// tests against failed lookups
if (id == 13)
return null;
return new APIUser
{
Id = id,
Username = usernames[id % usernames.Length],
};
}).ToList();
lookupUsersRequest.TriggerSuccess(new GetUsersResponse { Users = users });
return true;
default:
return false;
}
};
Children = new Drawable[]
{
lookupCache,
spectatorClient,
metadataClient,
new DependencyProvidingContainer
@@ -50,13 +91,9 @@ namespace osu.Game.Tests.Visual.Online
{
(typeof(SpectatorClient), spectatorClient),
(typeof(MetadataClient), metadataClient),
(typeof(UserLookupCache), lookupCache),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Purple)),
},
Child = currentlyOnline = new CurrentlyOnlineDisplay
{
RelativeSizeAxes = Axes.Both,
}
Child = currentlyOnline = new CurrentlyOnlineDisplay()
},
};
});
@@ -69,17 +106,18 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().FirstOrDefault()?.User.Id == 2);
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().First().CanSpectate.Value, () => Is.False);
AddStep("User began playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() }));
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().First().CanSpectate.Value, () => Is.True);
AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("User finished playing",
() => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().First().CanSpectate.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType<UserGridPanel>().Any());
AddUntilStep("Panel no longer present", () => !currentlyOnline.ChildrenOfType<OnlineUserPanel>().Any());
AddStep("End watching user presence", () => token.Dispose());
}
@@ -90,49 +128,14 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Begin watching user presence", () => token = metadataClient.BeginWatchingUserPresence());
AddStep("Add online user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.InSoloGame() }));
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<UserGridPanel>().FirstOrDefault()?.User.Id == streamingUser.Id);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.True);
AddUntilStep("Panel loaded", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().FirstOrDefault()?.User.Id == streamingUser.Id);
AddAssert("Spectate button enabled", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().First().CanSpectate.Value, () => Is.True);
AddStep("User finished playing", () => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<PurpleRoundedButton>().First().Enabled.Value, () => Is.False);
AddStep("User finished playing",
() => metadataClient.UserPresenceUpdated(streamingUser.Id, new UserPresence { Status = UserStatus.Online, Activity = new UserActivity.ChoosingBeatmap() }));
AddAssert("Spectate button disabled", () => currentlyOnline.ChildrenOfType<OnlineUserPanel>().First().CanSpectate.Value, () => Is.False);
AddStep("Remove playing user", () => metadataClient.UserPresenceUpdated(streamingUser.Id, null));
AddStep("End watching user presence", () => token.Dispose());
}
internal partial class TestUserLookupCache : UserLookupCache
{
private static readonly string[] usernames =
{
"fieryrage",
"Kerensa",
"MillhioreF",
"Player01",
"smoogipoo",
"Ephemeral",
"BTMC",
"Cilvery",
"m980",
"HappyStick",
"LittleEndu",
"frenzibyte",
"Zallius",
"BanchoBot",
"rocketminer210",
"pishifat"
};
protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{
// tests against failed lookups
if (lookup == 13)
return Task.FromResult<APIUser?>(null);
return Task.FromResult<APIUser?>(new APIUser
{
Id = lookup,
Username = usernames[lookup % usernames.Length],
});
}
}
}
}
@@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard.CurrentlyOnline;
using osu.Game.Rulesets;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneOnlineUserPanel : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Resolved]
private IRulesetStore rulesetStore { get; set; } = null!;
private TestMetadataClient metadataClient = null!;
private OnlineUserListPanel panel = null!;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(MetadataClient), metadataClient = new TestMetadataClient())
],
Children = new Drawable[]
{
metadataClient,
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(10f),
Children = new Drawable[]
{
new OnlineUserGridPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
WasRecentlyOnline = true
}),
new OnlineUserGridPanel(new APIUser
{
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true,
SupportLevel = 3,
}),
new OnlineUserListPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
WasRecentlyOnline = true
}),
panel = new OnlineUserListPanel(new APIUser
{
Username = @"peppy",
Id = 2,
CountryCode = CountryCode.AU,
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
LastVisit = DateTimeOffset.Now
}),
}
}
}
};
metadataClient.BeginWatchingUserPresence();
});
[Test]
public void TestUserActivity()
{
AddStep("idle", () => setPresence(UserStatus.Online, null));
AddStep("in game", () => setPresence(UserStatus.Online, new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(0)!)));
}
private void setPresence(UserStatus status, UserActivity? activity, int? userId = null)
{
if (status == UserStatus.Offline)
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, null);
else
metadataClient.UserPresenceUpdated(userId ?? panel.User.OnlineID, new UserPresence { Status = status, Activity = activity });
}
}
}
@@ -240,6 +240,7 @@ namespace osu.Game.Tests.Visual.Online
CoverUrl = TestResources.COVER_IMAGE_1,
JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now,
PreviousUsernames = ["ForgetMe", "MySpaceLover", "i once was a man named enis", "mr anderson"],
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
@@ -71,6 +71,15 @@ namespace osu.Game.Tests.Visual.Settings
checkBinding("Increase volume", "Shift");
}
[Test]
public void TestRulesetBindingSingleModifier()
{
scrollToAndStartBinding("Left button");
AddStep("press left shift", () => InputManager.Key(Key.ShiftLeft));
AddStep("release left shift", () => InputManager.ReleaseKey(Key.ShiftLeft));
checkBinding("Left button", "LShift");
}
[Test]
public void TestBindingSingleKeyWithModifier()
{
@@ -44,6 +44,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
protected TestBeatmapCarousel Carousel = null!;
protected bool RetainSelection { get; set; }
protected OsuScrollContainer<Drawable> Scroll => Carousel.ChildrenOfType<OsuScrollContainer<Drawable>>().Single();
[Cached(typeof(BeatmapStore))]
@@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Dependencies.Cache(Realm);
}
protected void CreateCarousel()
protected void CreateCarousel(bool retainSelection = false)
{
AddStep("create components", () =>
{
@@ -87,6 +89,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
BeatmapRecommendationFunction = null;
NewItemsPresentedInvocationCount = 0;
GroupedBeatmap? previousSelection = retainSelection ? Carousel.CurrentGroupedBeatmap : null;
Box topBox;
Children = new Drawable[]
{
@@ -120,6 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
Carousel = new TestBeatmapCarousel
{
CurrentGroupedBeatmap = previousSelection,
NewItemsPresented = _ => NewItemsPresentedInvocationCount++,
RequestSelection = b =>
{
@@ -23,6 +23,33 @@ namespace osu.Game.Tests.Visual.SongSelectV2
SortAndGroupBy(SortMode.Title, GroupMode.Length);
}
[Test]
public void TestInitialVisualState()
{
AddBeatmaps(3, splitApart: true);
WaitForDrawablePanels();
SelectNextSet();
WaitForSetSelection(set: 0, diff: 0);
AddAssert("selected item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
AddAssert("has visually expanded set", () => Carousel.GetCarouselItems()!.Count(item => item.Model is GroupedBeatmapSet && item.IsExpanded && item.IsVisible), () => Is.EqualTo(1));
CreateCarousel(retainSelection: true);
WaitForDrawablePanels();
WaitForSetSelection(set: 0, diff: 0);
AddAssert("selected item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
AddAssert("has visually expanded set", () => Carousel.GetCarouselItems()!.Count(item => item.Model is GroupedBeatmapSet && item.IsExpanded && item.IsVisible), () => Is.EqualTo(1));
CreateCarousel(retainSelection: true);
WaitForDrawablePanels();
WaitForSetSelection(set: 0, diff: 0);
AddAssert("selected item is visible", () => GetSelectedPanel()?.Item?.IsVisible, () => Is.True);
AddAssert("has visually expanded set", () => Carousel.GetCarouselItems()!.Count(item => item.Model is GroupedBeatmapSet && item.IsExpanded && item.IsVisible), () => Is.EqualTo(1));
}
[Test]
public void TestSetTraversal()
{
@@ -7,8 +7,10 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.SelectV2;
@@ -53,8 +55,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
working.Metadata.Source = string.Empty;
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("no success rate", () =>
{
@@ -63,8 +65,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.Beatmaps.Single().PlayCount = 0;
online.Result!.Beatmaps.Single().PassCount = 0;
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("no user ratings", () =>
{
@@ -72,8 +74,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.Ratings = Array.Empty<int>();
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("no fail times", () =>
{
@@ -81,8 +83,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.Beatmaps.Single().FailTimes = null;
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("no metrics", () =>
{
@@ -91,8 +93,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.Ratings = Array.Empty<int>();
online.Result!.Beatmaps.Single().FailTimes = null;
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("local beatmap", () =>
{
@@ -100,8 +102,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
working.BeatmapInfo.OnlineID = 0;
onlineLookupResult.Value = null;
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(null);
});
}
@@ -119,8 +121,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.Language = new BeatmapSetOnlineLanguage { Id = 12, Name = "Verrrrryyyy llooonngggggg language" };
online.Result!.Beatmaps.Single().TopTags = Enumerable.Repeat(online.Result!.Beatmaps.Single().TopTags, 3).SelectMany(t => t!).ToArray();
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
}
@@ -137,20 +139,28 @@ namespace osu.Game.Tests.Visual.SongSelectV2
working.BeatmapInfo.ResetOnlineInfo();
onlineLookupResult.Value = lookupResult;
Beatmap.Value = working;
onlineLookupResult.Value = lookupResult;
});
AddUntilStep("rating wedge hidden", () => !wedge.RatingsVisible);
AddUntilStep("fail time wedge hidden", () => !wedge.FailRetryVisible);
// just check for text everywhere on the wedge as the classes are private and generic
AddAssert("genre is still visible", () => wedge.ChildrenOfType<OsuSpriteText>().Any(t => t.Text == "Pop"));
AddAssert("language is still visible", () => wedge.ChildrenOfType<OsuSpriteText>().Any(t => t.Text == "English"));
AddStep("local beatmap", () =>
{
var (working, _) = createTestBeatmap();
onlineLookupResult.Value = null;
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.Completed(null);
});
AddAssert("rating wedge still hidden", () => !wedge.RatingsVisible);
AddAssert("fail time wedge still hidden", () => !wedge.FailRetryVisible);
AddAssert("genre is cleared", () => wedge.ChildrenOfType<OsuSpriteText>().All(t => t.Text != "Pop"));
AddAssert("language is cleared", () => wedge.ChildrenOfType<OsuSpriteText>().All(t => t.Text != "English"));
}
[Test]
@@ -166,8 +176,8 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
onlineLookupResult.Value = online;
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
}
@@ -178,9 +188,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
var (working, online) = createTestBeatmap();
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
@@ -192,9 +202,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.RelatedTags[1].Name = "another/tag";
online.Result!.RelatedTags[2].Name = "some/tag";
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
@@ -206,9 +216,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
@@ -220,9 +230,9 @@ namespace osu.Game.Tests.Visual.SongSelectV2
online.Result!.RelatedTags = null;
working.BeatmapSetInfo.Beatmaps.Single().Metadata.UserTags.Clear();
Beatmap.Value = working;
onlineLookupResult.Value = Screens.SelectV2.SongSelect.BeatmapSetLookupResult.InProgress();
Scheduler.AddDelayed(() => onlineLookupResult.Value = online, 500);
Beatmap.Value = working;
});
AddWaitStep("wait", 5);
}
@@ -197,15 +197,15 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep("favourites count is 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,345"));
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
AddStep("allow request to complete", () => resetEvent.Set());
AddStep("allow request to complete", resetEvent.Set);
AddUntilStep("favourites count is 2346", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,346"));
AddStep("reset event", () => resetEvent.Reset());
AddStep("reset event", resetEvent.Reset);
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
AddStep("allow request to complete", () => resetEvent.Set());
AddStep("allow request to complete", resetEvent.Set);
AddUntilStep("favourites count is 2345", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("2,345"));
AddStep("reset event", () => resetEvent.Reset());
AddStep("reset event", resetEvent.Reset);
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
AddStep("change to another beatmap", () =>
{
@@ -217,7 +217,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
Beatmap.Value = working;
onlineLookupResult.Value = online;
});
AddStep("allow request to complete", () => resetEvent.Set());
AddStep("allow request to complete", resetEvent.Set);
AddUntilStep("favourites count is 9999", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().Text.ToString(), () => Is.EqualTo("9,999"));
AddStep("set up request handler to fail", () =>
@@ -239,11 +239,11 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
};
});
AddStep("reset event", () => resetEvent.Reset());
AddStep("reset event", resetEvent.Reset);
AddStep("click favourite button", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single().TriggerClick());
AddAssert("spinner visible", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single()
.ChildrenOfType<LoadingSpinner>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
AddStep("allow request to complete", () => resetEvent.Set());
AddStep("allow request to complete", resetEvent.Set);
AddAssert("spinner hidden", () => this.ChildrenOfType<BeatmapTitleWedge.FavouriteButton>().Single()
.ChildrenOfType<LoadingSpinner>().Single().State.Value, () => Is.EqualTo(Visibility.Hidden));
}
@@ -295,7 +295,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
AddUntilStep($"displayed bpm is {target}", () =>
{
var label = titleWedge.ChildrenOfType<BeatmapTitleWedge.Statistic>().Single(l => l.TooltipText == BeatmapsetsStrings.ShowStatsBpm);
return label.Text == target;
return label.Text.ToString() == target;
});
}
@@ -40,20 +40,20 @@ namespace osu.Game.Tests.Visual.UserInterface
firedText
};
AddStep("start confirming", () => overlay.Begin());
AddStep("abort confirming", () => overlay.Abort());
AddStep("start confirming", overlay.Begin);
AddStep("abort confirming", overlay.Abort);
AddAssert("ensure not fired internally", () => !overlay.Fired);
AddAssert("ensure aborted", () => !fired);
AddStep("start confirming", () => overlay.Begin());
AddStep("start confirming", overlay.Begin);
AddUntilStep("wait until confirmed", () => fired);
AddAssert("ensure fired internally", () => overlay.Fired);
AddStep("abort after fire", () => overlay.Abort());
AddStep("abort after fire", overlay.Abort);
AddAssert("ensure not fired internally", () => !overlay.Fired);
AddStep("start confirming", () => overlay.Begin());
AddStep("start confirming", overlay.Begin);
AddUntilStep("wait until fired again", () => overlay.Fired);
}
@@ -133,9 +133,9 @@ namespace osu.Game.Tests.Visual.UserInterface
var loadedBackgrounds = backgrounds.Where(b => b.ContentLoaded);
AddUntilStep("some loaded", () => loadedBackgrounds.Any());
AddUntilStep("some loaded", loadedBackgrounds.Any);
AddStep("scroll to bottom", () => scrollContainer.ScrollToEnd());
AddUntilStep("all unloaded", () => !loadedBackgrounds.Any());
AddUntilStep("all unloaded", loadedBackgrounds.Any, () => Is.False);
}
private partial class TestUpdateableBeatmapBackgroundSprite : UpdateableBeatmapBackgroundSprite
@@ -89,9 +89,9 @@ namespace osu.Game.Tests.Visual.UserInterface
var loadedCovers = covers.Where(c => c.ChildrenOfType<OnlineBeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any());
AddUntilStep("some loaded", loadedCovers.Any);
AddStep("scroll to end", () => scroll.ScrollToEnd());
AddUntilStep("all unloaded", () => !loadedCovers.Any());
AddUntilStep("all unloaded", loadedCovers.Any, () => Is.False);
}
[Test]
@@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
OsuSpriteText sort;
OsuSpriteText displayStyle;
Add(toolbar = new UserListToolbar
Add(toolbar = new UserListToolbar(true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -98,7 +98,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f,
Margin = new MarginPadding(10),
Text = "Add beatmap",
Action = () => beatmapEditor.CreateNew()
Action = beatmapEditor.CreateNew
},
beatmapEditor
}
@@ -80,7 +80,7 @@ namespace osu.Game.Tournament.Screens.Editors
Width = 0.2f,
Margin = new MarginPadding(10),
Text = "Add beatmap",
Action = () => beatmapEditor.CreateNew()
Action = beatmapEditor.CreateNew
},
beatmapEditor
}
@@ -150,7 +150,7 @@ namespace osu.Game.Tournament.Screens.Editors
new SettingsButton
{
Text = "Add player",
Action = () => playerEditor.CreateNew()
Action = playerEditor.CreateNew
},
new Container
{
+37 -5
View File
@@ -75,6 +75,13 @@ namespace osu.Game.Beatmaps
currentMods.BindValueChanged(mods =>
{
// A change in bindable here doesn't guarantee that mods have actually changed.
// However, we *do* want to make sure that the mod *references* are the same;
// `SequenceEqual()` without a comparer would fall back to `IEquatable`.
// Failing to ensure reference equality can cause setting change tracking to fail later.
if (mods.OldValue.SequenceEqual(mods.NewValue, ReferenceEqualityComparer.Instance))
return;
modSettingChangeTracker?.Dispose();
Scheduler.AddOnce(updateTrackedBindables);
@@ -82,15 +89,37 @@ namespace osu.Game.Beatmaps
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += _ =>
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
lock (bindableUpdateLock)
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = Scheduler.AddDelayed(updateTrackedBindables, 100);
}
};
}, true);
}
public void Invalidate(IBeatmapInfo beatmap)
/// <summary>
/// Notify this cache that a beatmap has been invalidated/updated.
/// </summary>
/// <param name="oldBeatmap">The old beatmap model.</param>
/// <param name="newBeatmap">The updated beatmap model.</param>
public void Invalidate(IBeatmapInfo oldBeatmap, IBeatmapInfo newBeatmap)
{
base.Invalidate(lookup => lookup.BeatmapInfo.Equals(beatmap));
base.Invalidate(lookup => lookup.BeatmapInfo.Equals(oldBeatmap));
lock (bindableUpdateLock)
{
bool trackedBindablesRefreshRequired = false;
foreach (var bsd in trackedBindables.Where(bsd => bsd.BeatmapInfo.Equals(oldBeatmap)))
{
bsd.BeatmapInfo = newBeatmap;
trackedBindablesRefreshRequired = true;
}
if (trackedBindablesRefreshRequired)
Scheduler.AddOnce(updateTrackedBindables);
}
}
/// <summary>
@@ -195,6 +224,9 @@ namespace osu.Game.Beatmaps
{
lock (bindableUpdateLock)
{
debouncedModSettingsChange?.Cancel();
debouncedModSettingsChange = null;
trackedUpdateCancellationSource.Cancel();
trackedUpdateCancellationSource = new CancellationTokenSource();
@@ -348,7 +380,7 @@ namespace osu.Game.Beatmaps
private class BindableStarDifficulty : Bindable<StarDifficulty>
{
public readonly IBeatmapInfo BeatmapInfo;
public IBeatmapInfo BeatmapInfo;
public readonly CancellationToken CancellationToken;
public BindableStarDifficulty(IBeatmapInfo beatmapInfo, CancellationToken cancellationToken)
+3 -3
View File
@@ -52,11 +52,11 @@ namespace osu.Game.Beatmaps
foreach (BeatmapInfo beatmap in beatmapSet.Beatmaps)
{
difficultyCache.Invalidate(beatmap);
var working = workingBeatmapCache.GetWorkingBeatmap(beatmap);
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
difficultyCache.Invalidate(beatmap, working.BeatmapInfo);
var ruleset = working.BeatmapInfo.Ruleset.CreateInstance();
var calculator = ruleset.CreateDifficultyCalculator(working);
beatmap.StarRating = calculator.Calculate().StarRating;
@@ -170,21 +170,21 @@ namespace osu.Game.Beatmaps.Formats
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.StartTime + CONTROL_POINT_LENIENCY + 1)
?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
hitObject.Samples = hitObject.Samples.Select(sampleControlPoint.ApplyTo).ToList();
for (int i = 0; i < hasRepeats.NodeSamples.Count; i++)
{
double time = hitObject.StartTime + i * hasRepeats.Duration / hasRepeats.SpanCount() + CONTROL_POINT_LENIENCY;
var nodeSamplePoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(time) ?? SampleControlPoint.DEFAULT;
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(o => nodeSamplePoint.ApplyTo(o)).ToList();
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(nodeSamplePoint.ApplyTo).ToList();
}
}
else
{
SampleControlPoint sampleControlPoint = (beatmap.ControlPointInfo as LegacyControlPointInfo)?.SamplePointAt(hitObject.GetEndTime() + CONTROL_POINT_LENIENCY)
?? SampleControlPoint.DEFAULT;
hitObject.Samples = hitObject.Samples.Select(o => sampleControlPoint.ApplyTo(o)).ToList();
hitObject.Samples = hitObject.Samples.Select(sampleControlPoint.ApplyTo).ToList();
}
}
+11
View File
@@ -4,6 +4,7 @@
#nullable disable
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using JetBrains.Annotations;
@@ -104,6 +105,10 @@ namespace osu.Game.Beatmaps
beatmapInfo = beatmapInfo.Detach();
// If this ever gets hit, a request has arrived with an outdated BeatmapInfo.
// An outdated BeatmapInfo may contain a reference to a previous version of the beatmap's files on disk.
Debug.Assert(confirmFileHashIsUpToDate(beatmapInfo), "working beatmap returned with outdated path");
workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this));
// best effort; may be higher than expected.
@@ -113,6 +118,12 @@ namespace osu.Game.Beatmaps
}
}
private bool confirmFileHashIsUpToDate(BeatmapInfo beatmapInfo)
{
string refetchPath = realm.Run(r => r.Find<BeatmapInfo>(beatmapInfo.ID)?.File?.File.Hash);
return refetchPath == null || refetchPath == beatmapInfo.File?.File.Hash;
}
#region IResourceStorageProvider
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
@@ -15,6 +15,7 @@ using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Dashboard.Friends;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Edit.Compose.Components;
@@ -234,6 +235,9 @@ namespace osu.Game.Configuration
// intentionally uses `DateTime?` and not `DateTimeOffset?` because the latter fails due to `DateTimeOffset` not implementing `IConvertible`
SetDefault(OsuSetting.LastOnlineTagsPopulation, (DateTime?)null);
SetDefault(OsuSetting.DashboardSortMode, UserSortCriteria.LastVisit);
SetDefault(OsuSetting.DashboardDisplayStyle, OverlayPanelDisplayStyle.Card);
}
protected override bool CheckLookupContainsPrivateInformation(OsuSetting lookup)
@@ -486,5 +490,8 @@ namespace osu.Game.Configuration
LastOnlineTagsPopulation,
AutomaticallyAdjustBeatmapOffset,
DashboardSortMode,
DashboardDisplayStyle,
}
}
@@ -140,7 +140,7 @@ namespace osu.Game.Configuration
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
KeyboardStep = bNumber.Precision,
};
break;
@@ -151,7 +151,7 @@ namespace osu.Game.Configuration
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber,
KeyboardStep = 0.1f,
KeyboardStep = (float)bNumber.Precision,
};
break;
@@ -161,7 +161,8 @@ namespace osu.Game.Configuration
{
LabelText = attr.Label,
TooltipText = attr.Description,
Current = bNumber
Current = bNumber,
KeyboardStep = bNumber.Precision,
};
break;
@@ -76,6 +76,15 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count;
}
/// <summary>
/// Completely purge the cache.
/// </summary>
public virtual void Clear()
{
cache.Clear();
statistics.Value.Usage = 0;
}
protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) =>
cache.TryGetValue(lookup, out value);
@@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
namespace osu.Game.Graphics.Cursor
{
@@ -93,10 +94,10 @@ namespace osu.Game.Graphics.Cursor
protected override void PopIn()
{
instantMovement |= !IsPresent;
this.FadeIn(500, Easing.OutQuint);
this.FadeIn(300, Easing.OutQuint);
}
protected override void PopOut() => this.Delay(150).FadeOut(500, Easing.OutQuint);
protected override void PopOut() => this.Delay(150).FadeOut(300, Easing.OutQuint);
public override void Move(Vector2 pos)
{
@@ -107,7 +108,8 @@ namespace osu.Game.Graphics.Cursor
}
else
{
this.MoveTo(pos, 200, Easing.OutQuint);
// This method is called every frame so we can do this safely here.
Position = Interpolation.ValueAt(Time.Elapsed, Position, pos, 0, 120, Easing.OutQuint);
}
}
}
@@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
@@ -21,10 +18,8 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class HoverClickSounds : HoverSounds
{
public Bindable<bool> Enabled = new Bindable<bool>(true);
private Sample sampleClick;
private Sample sampleClickDisabled;
private Sample? sampleClick;
private Sample? sampleClickDisabled;
private readonly MouseButton[] buttons;
@@ -36,7 +31,7 @@ namespace osu.Game.Graphics.UserInterface
/// Array of button codes which should trigger the click sound.
/// If this optional parameter is omitted or set to <code>null</code>, the click sound will only be played on left click.
/// </param>
public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Default, MouseButton[] buttons = null)
public HoverClickSounds(HoverSampleSet sampleSet = HoverSampleSet.Default, MouseButton[]? buttons = null)
: base(sampleSet)
{
this.buttons = buttons ?? new[] { MouseButton.Left };
@@ -60,14 +55,6 @@ namespace osu.Game.Graphics.UserInterface
return base.OnClick(e);
}
public override void PlayHoverSample()
{
if (!Enabled.Value)
return;
base.PlayHoverSample();
}
public void PlayClickSample()
{
var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel();
@@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Utils;
@@ -18,6 +19,8 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class HoverSounds : HoverSampleDebounceComponent
{
public readonly Bindable<bool> Enabled = new Bindable<bool>(true);
private Sample sampleHover;
protected readonly HoverSampleSet SampleSet;
@@ -37,6 +40,9 @@ namespace osu.Game.Graphics.UserInterface
public override void PlayHoverSample()
{
if (!Enabled.Value)
return;
sampleHover.Frequency.Value = 0.98 + RNG.NextDouble(0.04);
sampleHover.Play();
}
@@ -104,9 +104,17 @@ namespace osu.Game.Graphics.UserInterface
}
}
private Vector2? targetSize;
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
protected override void UpdateSize(Vector2 newSize)
{
// TODO: should probably fix this at a framework level (this method is running every frame which can spam transforms)
if (newSize == targetSize)
return;
targetSize = newSize;
if (Direction == Direction.Vertical)
{
Width = newSize.X;
@@ -42,6 +42,8 @@ namespace osu.Game.Graphics.UserInterface
Margin = new MarginPadding { Left = 2 },
};
protected bool DrawBorder { get; init; } = true;
private OsuCaret? caret;
private bool selectionStarted;
@@ -256,7 +258,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnFocus(FocusEvent e)
{
if (Masking)
if (DrawBorder)
BorderThickness = 3;
base.OnFocus(e);
@@ -268,7 +270,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void OnFocusLost(FocusLostEvent e)
{
if (Masking)
if (DrawBorder)
BorderThickness = 0;
base.OnFocusLost(e);
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@@ -22,7 +23,7 @@ namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
Origin = Anchor.CentreLeft;
LabelTextFlowContainer.Anchor = Anchor.CentreLeft;
LabelTextFlowContainer.Origin = Anchor.CentreLeft;
LabelText = @"Show hidden";
LabelText = UserInterfaceStrings.ShowHidden;
Scale = new Vector2(0.8f);
}
@@ -8,7 +8,9 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
@@ -26,9 +28,9 @@ namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
d.Alpha = 0;
});
protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayComputer();
protected override DirectorySelectorDirectory CreateRootDirectoryItem() => new OsuBreadcrumbDisplayDevice();
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, LocalisableString? displayName = null) => new OsuBreadcrumbDisplayDirectory(directory, displayName);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@@ -47,19 +49,19 @@ namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
});
}
private partial class OsuBreadcrumbDisplayComputer : OsuBreadcrumbDisplayDirectory
private partial class OsuBreadcrumbDisplayDevice : OsuBreadcrumbDisplayDirectory
{
protected override IconUsage? Icon => null;
public OsuBreadcrumbDisplayComputer()
: base(null, "Computer")
public OsuBreadcrumbDisplayDevice()
: base(null, UserInterfaceStrings.Device)
{
}
}
private partial class OsuBreadcrumbDisplayDirectory : DirectorySelectorDirectory
{
public OsuBreadcrumbDisplayDirectory(DirectoryInfo? directory, string? displayName = null)
public OsuBreadcrumbDisplayDirectory(DirectoryInfo? directory, LocalisableString? displayName = null)
: base(directory, displayName)
{
}
@@ -6,13 +6,14 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterfaceV2.FileSelection
{
internal partial class OsuDirectorySelectorDirectory : DirectorySelectorDirectory
{
public OsuDirectorySelectorDirectory(DirectoryInfo directory, string? displayName = null)
public OsuDirectorySelectorDirectory(DirectoryInfo directory, LocalisableString? displayName = null)
: base(directory, displayName)
{
}
+11 -33
View File
@@ -9,11 +9,9 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -70,8 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Container content = null!;
private Box background = null!;
private FormControlBackground background = null!;
private OsuTextFlowContainer text = null!;
private Button button = null!;
@@ -81,7 +78,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = content = new Container
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -90,21 +87,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
CornerExponent = 2.5f,
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new TrianglesV2
{
SpawnRatio = 0.5f,
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(colourProvider.Background4, colourProvider.Background5),
},
new HoverClickSounds(HoverSampleSet.Button)
{
Enabled = { BindTarget = Enabled },
},
background = new FormControlBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
@@ -175,7 +158,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
if (Enabled.Value)
{
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
background.Flash();
button.TriggerClick();
}
@@ -186,19 +169,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
text.Colour = Enabled.Value ? colourProvider.Content1 : colourProvider.Background1;
background.FadeColour(IsHovered
? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4)
: colourProvider.Background5, 200, Easing.OutQuint);
content.BorderThickness = IsHovered ? 2 : 0;
if (BackgroundColour != null)
{
button.BackgroundColour = BackgroundColour.Value;
content.BorderColour = Enabled.Value ? BackgroundColour.Value : Interpolation.ValueAt(0.75, BackgroundColour.Value, colourProvider.Dark1, 0, 1);
}
if (!Enabled.Value)
background.VisualStyle = VisualStyle.Disabled;
else if (IsHovered)
background.VisualStyle = VisualStyle.Hovered;
else
content.BorderColour = Enabled.Value ? colourProvider.Light4 : colourProvider.Dark1;
background.VisualStyle = VisualStyle.Normal;
// TODO: Support BackgroundColour?
}
public partial class Button : OsuButton
@@ -4,21 +4,14 @@
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.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
@@ -42,34 +35,23 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString HintText { get; init; }
private Box background = null!;
private FormControlBackground background = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText text = null!;
private Sample? sampleChecked;
private Sample? sampleUnchecked;
private Sample? sampleDisabled;
private SwitchButton switchButton = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
background = new FormControlBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
@@ -77,13 +59,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
Padding = new MarginPadding(9),
Children = new Drawable[]
{
new FillFlowContainer
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding { Right = SwitchButton.WIDTH + 5 },
Spacing = new Vector2(0f, 4f),
Children = new Drawable[]
{
caption = new FormFieldCaption
@@ -91,13 +73,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
Caption = Caption,
TooltipText = HintText,
},
text = new OsuSpriteText
{
RelativeSizeAxes = Axes.X,
},
},
},
new SwitchButton
switchButton = new SwitchButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@@ -106,9 +84,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
},
};
sampleChecked = audio.Samples.Get(@"UI/check-on");
sampleUnchecked = audio.Samples.Get(@"UI/check-off");
sampleDisabled = audio.Samples.Get(@"UI/default-select-disabled");
}
protected override void LoadComplete()
@@ -118,22 +93,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
current.BindValueChanged(_ =>
{
updateState();
playSamples();
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
background.Flash();
ValueChanged?.Invoke();
});
current.BindDisabledChanged(_ => updateState(), true);
}
private void playSamples()
{
if (Current.Value)
sampleChecked?.Play();
else
sampleUnchecked?.Play();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
@@ -148,28 +114,20 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override bool OnClick(ClickEvent e)
{
if (!Current.Disabled)
Current.Value = !Current.Value;
else
sampleDisabled?.Play();
switchButton.TriggerClick();
return true;
}
private void updateState()
{
caption.Colour = Current.Disabled ? colourProvider.Background1 : colourProvider.Content2;
text.Colour = Current.Disabled ? colourProvider.Background1 : colourProvider.Content1;
text.Text = Current.Value ? CommonStrings.Enabled : CommonStrings.Disabled;
// use FadeColour to override any existing colour transform (i.e. FlashColour on click).
background.FadeColour(IsHovered
? ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4)
: colourProvider.Background5);
BorderThickness = IsHovered ? 2 : 0;
BorderColour = Current.Disabled ? colourProvider.Dark1 : colourProvider.Light4;
if (IsDisabled)
background.VisualStyle = VisualStyle.Disabled;
else if (IsHovered)
background.VisualStyle = VisualStyle.Hovered;
else
background.VisualStyle = VisualStyle.Normal;
}
public IEnumerable<LocalisableString> FilterTerms => Caption.Yield();
@@ -181,5 +139,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public void SetDefault() => Current.SetDefault();
public bool IsDisabled => Current.Disabled;
public float MainDrawHeight => DrawHeight;
}
}
@@ -33,7 +33,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public BindableBool CanAdd { get; } = new BindableBool(true);
private Box background = null!;
private FormControlBackground background = null!;
private FormFieldCaption caption = null!;
private FillFlowContainer flow = null!;
private RoundedButton addButton = null!;
@@ -47,16 +47,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
background = new FormControlBackground(),
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
@@ -140,13 +133,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState()
{
background.Colour = colourProvider.Background5;
caption.Colour = colourProvider.Content2;
BorderThickness = IsHovered ? 2 : 0;
if (IsHovered)
BorderColour = colourProvider.Light4;
background.VisualStyle = VisualStyle.Hovered;
else
background.VisualStyle = VisualStyle.Normal;
}
private void updateColours()
@@ -0,0 +1,125 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormControlBackground : CompositeDrawable
{
public const float CORNER_EXPONENT = 2.5f;
public const float BORDER_THICKNESS = 2.5f;
private VisualStyle visualStyle;
public VisualStyle VisualStyle
{
get => visualStyle;
set
{
visualStyle = value;
updateStyle();
}
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private readonly Box box;
private readonly HoverSounds sounds;
public FormControlBackground()
{
RelativeSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 5;
CornerExponent = CORNER_EXPONENT;
BorderThickness = BORDER_THICKNESS;
InternalChildren = new Drawable[]
{
box = new Box
{
Colour = Color4.White,
RelativeSizeAxes = Axes.Both,
},
sounds = new HoverSounds(),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateStyle();
FinishTransforms(true);
}
public void Flash()
{
box.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
}
private void updateStyle()
{
sounds.Enabled.Value = visualStyle != VisualStyle.Disabled;
ColourInfo colour;
ColourInfo borderColour;
bool border = false;
switch (visualStyle)
{
case VisualStyle.Normal:
colour = colourProvider.Background4.Darken(0.1f);
borderColour = colourProvider.Light4;
break;
case VisualStyle.Disabled:
colour = colourProvider.Background4;
borderColour = colourProvider.Dark1;
break;
case VisualStyle.Hovered:
colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
borderColour = colourProvider.Light4;
border = true;
break;
case VisualStyle.Focused:
colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
border = true;
borderColour = colourProvider.Highlight1;
break;
default:
throw new ArgumentOutOfRangeException();
}
this.TransformTo(nameof(BorderColour), border ? borderColour : colour, 250, Easing.OutQuint);
box.FadeColour(colour, 250, Easing.OutQuint);
}
}
public enum VisualStyle
{
Normal,
Disabled,
Hovered,
Focused
}
}
@@ -53,10 +53,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
header.Caption = Caption;
header.HintText = HintText;
// there's bottom margin applied inside the header to give spacing between the header and the menu.
// however when the menu is closed the extra spacing remains present. to remove it, apply negative bottom padding here.
Margin = new MarginPadding { Bottom = -header_menu_spacing };
}
protected override void LoadComplete()
@@ -84,6 +80,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
public bool IsDisabled => Current.Disabled;
public float MainDrawHeight => header.DrawHeight;
protected override DropdownHeader CreateHeader() => header = new FormDropdownHeader
{
Dropdown = this,
@@ -145,6 +143,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private FormFieldCaption caption = null!;
private OsuSpriteText label = null!;
private SpriteIcon chevron = null!;
private FormControlBackground background = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -155,46 +154,51 @@ namespace osu.Game.Graphics.UserInterfaceV2
Masking = true;
CornerRadius = 5;
Margin = new MarginPadding { Bottom = header_menu_spacing };
// We use our own background for more control.
Background.Alpha = 0;
Foreground.Padding = new MarginPadding(9);
Foreground.Children = new Drawable[]
{
new FillFlowContainer
background = new FormControlBackground(),
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Padding = new MarginPadding(9),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
label = new TruncatingSpriteText
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = 25 },
AlwaysPresent = true,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4),
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
label = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = 25 },
AlwaysPresent = true,
},
}
},
chevron = new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Size = new Vector2(16),
Margin = new MarginPadding { Right = 5 },
},
}
},
chevron = new SpriteIcon
{
Icon = FontAwesome.Solid.ChevronDown,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(16),
Margin = new MarginPadding { Right = 5 },
},
};
AddInternal(new HoverClickSounds
{
Enabled = { BindTarget = Enabled },
});
}
protected override void LoadComplete()
@@ -240,25 +244,21 @@ namespace osu.Game.Graphics.UserInterfaceV2
else
label.Alpha = 1;
BorderThickness = IsHovered || dropdownOpen ? 2 : 0;
if (Dropdown.Current.Disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = dropdownOpen ? colourProvider.Highlight1 : colourProvider.Light4;
if (dropdownOpen)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
background.VisualStyle = VisualStyle.Disabled;
else if (dropdownOpen)
background.VisualStyle = VisualStyle.Focused;
else if (IsHovered)
Background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
background.VisualStyle = VisualStyle.Hovered;
else
Background.Colour = colourProvider.Background5;
background.VisualStyle = VisualStyle.Normal;
}
private void updateChevron()
{
bool open = Dropdown.Menu.State == MenuState.Open;
chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
chevron.MoveToY(open ? -chevron.DrawHeight : 0, 300, Easing.OutQuint);
}
}
@@ -291,7 +291,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
ItemsContainer.Padding = new MarginPadding(9);
MaskingContainer.BorderThickness = 2;
MaskingContainer.BorderThickness = FormControlBackground.BORDER_THICKNESS;
MaskingContainer.CornerExponent = FormControlBackground.CORNER_EXPONENT;
MaskingContainer.BorderColour = colourProvider.Highlight1;
}
@@ -299,16 +300,16 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.AnimateOpen();
// there's negative bottom margin applied on the whole dropdown control to remove extra spacing when the menu is closed.
// however, when the menu is open, we want spacing between the menu and the next control below it. therefore apply bottom margin here.
// we use a transform to keep the open animation smooth while margin is adjusted.
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = header_menu_spacing }, 300, Easing.OutQuint);
this.TransformTo(nameof(Margin), new MarginPadding
{
Top = header_menu_spacing,
}, 300, Easing.OutQuint);
}
protected override void AnimateClose()
{
base.AnimateClose();
this.TransformTo(nameof(Margin), new MarginPadding { Bottom = 0 }, 300, Easing.OutQuint);
this.TransformTo(nameof(Margin), new MarginPadding(), 300, Easing.OutQuint);
}
}
}
@@ -50,7 +50,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = textFlow = new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold))
InternalChild = textFlow = new OsuTextFlowContainer(t => t.Font = OsuFont.Style.Caption1)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@@ -11,7 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
@@ -70,7 +69,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public Container PreviewContainer { get; private set; } = null!;
private Box background = null!;
private FormControlBackground background = null!;
private FormFieldCaption caption = null!;
private OsuSpriteText placeholderText = null!;
@@ -93,16 +92,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
background = new FormControlBackground(),
PreviewContainer = new Container
{
RelativeSizeAxes = Axes.X,
@@ -194,7 +186,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
initialChooserPath = Current.Value?.DirectoryName;
placeholderText.Alpha = Current.Value == null ? 1 : 0;
filenameText.Text = Current.Value?.Name ?? string.Empty;
background.FlashColour(ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark2), 800, Easing.OutQuint);
background.Flash();
}
protected override bool OnClick(ClickEvent e)
@@ -220,22 +212,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
filenameText.Colour = Current.Disabled || Current.Value == null ? colourProvider.Foreground1 : colourProvider.Content1;
if (!Current.Disabled)
{
BorderThickness = IsHovered || popoverState.Value == Visibility.Visible ? 2 : 0;
BorderColour = popoverState.Value == Visibility.Visible ? colourProvider.Highlight1 : colourProvider.Light4;
if (popoverState.Value == Visibility.Visible)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
else
background.Colour = colourProvider.Background5;
}
if (Current.Disabled)
background.VisualStyle = VisualStyle.Disabled;
else if (popoverState.Value == Visibility.Visible)
background.VisualStyle = VisualStyle.Focused;
else if (IsHovered)
background.VisualStyle = VisualStyle.Hovered;
else
{
background.Colour = colourProvider.Background4;
}
background.VisualStyle = VisualStyle.Normal;
}
protected override void Dispose(bool isDisposing)
@@ -22,6 +22,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osuTK.Graphics;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Graphics.UserInterfaceV2
@@ -121,7 +122,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public Func<T, LocalisableString> TooltipFormat { get; init; }
private Box background = null!;
private FormControlBackground background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private OsuSpriteText valueLabel = null!;
@@ -199,14 +200,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
background = new FormControlBackground(),
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
@@ -329,8 +327,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
currentNumberInstantaneous.TriggerChange();
current.Value = currentNumberInstantaneous.Value;
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint);
background.Flash();
}
private void tryUpdateSliderFromTextBox()
@@ -398,19 +395,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
valueLabel.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background1 : colourProvider.Content1;
BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0;
if (Current.Disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = childHasFocus ? colourProvider.Highlight1 : colourProvider.Light4;
if (childHasFocus)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
background.VisualStyle = VisualStyle.Disabled;
else if (childHasFocus)
background.VisualStyle = VisualStyle.Focused;
else if (IsHovered || slider.IsDragging.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
background.VisualStyle = VisualStyle.Hovered;
else
background.Colour = colourProvider.Background5;
background.VisualStyle = VisualStyle.Normal;
}
private void updateValueDisplay()
@@ -464,7 +456,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
private Box leftBox = null!;
private Box rightBox = null!;
private InnerSliderNub nub = null!;
private HoverClickSounds sounds = null!;
public const float NUB_WIDTH = 10;
[Resolved]
@@ -509,14 +500,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
ResetToDefault = ResetToDefault,
}
},
sounds = new HoverClickSounds()
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindDisabledChanged(_ => updateState(), true);
FinishTransforms(true);
}
protected override void UpdateAfterChildren()
@@ -569,24 +561,29 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState()
{
sounds.Enabled.Value = !Current.Disabled;
rightBox.Colour = colourProvider.Background6;
rightBox.Colour = colourProvider.Background5;
Color4 leftColour = colourProvider.Light4;
Color4 nubColour;
if (IsHovered || HasFocus || IsDragged)
nubColour = colourProvider.Highlight1;
else
nubColour = colourProvider.Highlight1.Darken(0.1f);
if (Current.Disabled)
{
leftBox.Colour = colourProvider.Dark3;
nub.Colour = colourProvider.Dark1;
}
else
{
leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f);
nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
nubColour = nubColour.Darken(0.4f);
leftColour = leftColour.Darken(0.4f);
}
leftBox.FadeColour(leftColour, 250, Easing.OutQuint);
nub.FadeColour(nubColour, 250, Easing.OutQuint);
}
protected override void UpdateValue(float value)
{
nub.MoveToX(value, 200, Easing.OutPow10);
nub.MoveToX(value, 250, Easing.OutElasticQuarter);
}
protected override bool Commit()
@@ -609,6 +606,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader]
private void load()
{
CornerExponent = 2.5f;
Width = InnerSlider.NUB_WIDTH;
RelativeSizeAxes = Axes.Y;
RelativePositionAxes = Axes.X;
@@ -633,5 +631,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public void SetDefault() => Current.SetDefault();
public bool IsDisabled => Current.Disabled;
public float MainDrawHeight => DrawHeight;
}
}
@@ -77,7 +77,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary>
public LocalisableString PlaceholderText { get; init; }
private Box background = null!;
private FormControlBackground background = null!;
private Box flashLayer = null!;
private InnerTextBox textBox = null!;
private FormFieldCaption caption = null!;
@@ -92,17 +92,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
CornerExponent = 2.5f;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
background = new FormControlBackground(),
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
@@ -193,19 +185,14 @@ namespace osu.Game.Graphics.UserInterfaceV2
caption.Colour = disabled ? colourProvider.Background1 : colourProvider.Content2;
textBox.Colour = disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = IsHovered || textBox.Focused.Value ? 2 : 0;
if (disabled)
BorderColour = colourProvider.Dark1;
else
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
if (textBox.Focused.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
if (Current.Disabled)
background.VisualStyle = VisualStyle.Disabled;
else if (textBox.Focused.Value)
background.VisualStyle = VisualStyle.Focused;
else if (IsHovered)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
background.VisualStyle = VisualStyle.Hovered;
else
background.Colour = colourProvider.Background5;
background.VisualStyle = VisualStyle.Normal;
}
internal partial class InnerTextBox : OsuTextBox
@@ -216,12 +203,16 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override float LeftRightPadding => 0;
public InnerTextBox()
{
DrawBorder = false;
}
[BackgroundDependencyLoader]
private void load()
{
Height = 16;
TextContainer.Height = 1;
Masking = false;
BackgroundUnfocused = BackgroundFocused = BackgroundCommit = Colour4.Transparent;
}
@@ -258,5 +249,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public bool IsDisabled => current.Disabled;
public IEnumerable<LocalisableString> FilterTerms => Caption.Yield();
public float MainDrawHeight => DrawHeight;
}
}
@@ -31,5 +31,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// Whether the control is currently disabled.
/// </summary>
bool IsDisabled { get; }
/// <summary>
/// The height of the main part of the control (when not expanded).
/// This is used to attach external elements.
/// </summary>
float MainDrawHeight { get; }
}
}
@@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
using osu.Game.Overlays;
@@ -67,7 +68,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, LocalisableString? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override void NotifySelectionError() => this.FlashColour(Colour4.Red, 300);
}
@@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2.FileSelection;
@@ -69,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override DirectorySelectorDirectory CreateParentDirectoryItem(DirectoryInfo directory) => new OsuDirectorySelectorParentDirectory(directory);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, string? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override DirectorySelectorDirectory CreateDirectoryItem(DirectoryInfo directory, LocalisableString? displayName = null) => new OsuDirectorySelectorDirectory(directory, displayName);
protected override DirectoryListingFile CreateFileItem(FileInfo file) => new OsuDirectoryListingFile(file);
@@ -19,32 +19,32 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class SwitchButton : Checkbox
{
public const float WIDTH = 45;
private const float border_thickness = 4.5f;
private const float padding = 1.25f;
public const float WIDTH = 56;
private readonly Box fill;
private readonly Container nubContainer;
private readonly Drawable nub;
private readonly CircularContainer content;
private readonly Container content;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public bool ExpandOnCurrent { get; init; } = true;
private Sample? sampleChecked;
private Sample? sampleUnchecked;
public SwitchButton()
{
Size = new Vector2(WIDTH, 20);
Size = new Vector2(WIDTH, 16);
InternalChild = content = new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
BorderColour = Color4.White,
BorderThickness = border_thickness,
BorderThickness = 3.2f,
Masking = true,
CornerExponent = 2.5f,
Children = new Drawable[]
{
fill = new Box
@@ -52,21 +52,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.Both,
AlwaysPresent = true,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(border_thickness + padding),
Child = nubContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = nub = new Circle
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Masking = true,
}
}
}
}
};
}
@@ -82,28 +67,21 @@ namespace osu.Game.Graphics.UserInterfaceV2
{
base.LoadComplete();
Current.BindDisabledChanged(_ => updateColours());
Current.BindDisabledChanged(_ => updateState());
Current.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private void updateState()
{
nub.MoveToX(Current.Value ? nubContainer.DrawWidth - nub.DrawWidth : 0, 200, Easing.OutQuint);
updateColours();
}
protected override bool OnHover(HoverEvent e)
{
updateColours();
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateColours();
updateState();
base.OnHoverLost(e);
}
@@ -121,35 +99,33 @@ namespace osu.Game.Graphics.UserInterfaceV2
sampleUnchecked?.Play();
}
private void updateColours()
private void updateState()
{
ColourInfo borderColour;
ColourInfo switchColour;
Color4 fillColour = colourProvider.Background5.Opacity(0);
Color4 borderColour = colourProvider.Light4;
if (IsHovered)
borderColour = colourProvider.Highlight1;
else if (Current.Value)
borderColour = colourProvider.Highlight1.Darken(0.1f);
if (Current.Value)
fillColour = borderColour;
if (Current.Disabled)
{
borderColour = colourProvider.Dark2;
switchColour = colourProvider.Dark1;
fill.Colour = colourProvider.Dark5;
fillColour = fillColour.Darken(0.4f);
borderColour = borderColour.Darken(0.4f);
}
fill.FadeColour(fillColour, 250, Easing.OutQuint);
content.TransformTo(nameof(BorderColour), (ColourInfo)borderColour, 250, Easing.OutQuint);
if (ExpandOnCurrent && Current.Value)
content.ResizeWidthTo(1f, 200, Easing.OutElasticQuarter);
else
{
bool hover = IsHovered && !Current.Disabled;
borderColour = hover ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Highlight1.Opacity(0.3f);
switchColour = hover || Current.Value ? colourProvider.Highlight1 : colourProvider.Light4;
if (!Current.Value)
{
borderColour = borderColour.MultiplyAlpha(0.8f);
switchColour = switchColour.MultiplyAlpha(0.8f);
}
fill.Colour = Current.Value ? colourProvider.Colour4.Darken(0.2f) : colourProvider.Background6;
}
nubContainer.FadeColour(switchColour, 250, Easing.OutQuint);
content.TransformTo(nameof(BorderColour), borderColour, 250, Easing.OutQuint);
content.ResizeWidthTo(0.75f, 120, Easing.OutExpo);
}
}
}
@@ -105,6 +105,8 @@ namespace osu.Game.Input.Bindings
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.E }, GlobalAction.PreviousSkin),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.T }, GlobalAction.NextSkin),
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
@@ -520,6 +522,12 @@ namespace osu.Game.Input.Bindings
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleCurrentGroup))]
ToggleCurrentGroup,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.PreviousSkin))]
PreviousSkin,
[LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.NextSkin))]
NextSkin,
}
public enum GlobalActionCategory
@@ -69,6 +69,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Date => new TranslatableString(getKey(@"date"), @"Date");
/// <summary>
/// "Personal Best"
/// </summary>
public static LocalisableString PersonalBest => new TranslatableString(getKey(@"personal_best"), @"Personal Best");
/// <summary>
/// "Personal Best (#{0:N0} of {1:N0})"
/// </summary>
public static LocalisableString PersonalBestWithPosition(int position, int totalCount) => new TranslatableString(getKey(@"personal_best_with_position"), @"Personal Best (#{0:N0} of {1:N0})", position, totalCount);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -10,9 +10,9 @@ namespace osu.Game.Localisation
private const string prefix = @"osu.Game.Resources.Localisation.BindingSettings";
/// <summary>
/// "Shortcut and gameplay bindings"
/// "Shortcuts and gameplay bindings"
/// </summary>
public static LocalisableString ShortcutAndGameplayBindings => new TranslatableString(getKey(@"shortcut_and_gameplay_bindings"), @"Shortcut and gameplay bindings");
public static LocalisableString ShortcutAndGameplayBindings => new TranslatableString(getKey(@"shortcut_and_gameplay_bindings"), @"Shortcuts and gameplay bindings");
/// <summary>
/// "Configure"
@@ -59,6 +59,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DailyChallenge => new TranslatableString(getKey(@"daily_challenge"), @"daily challenge");
/// <summary>
/// "lounge"
/// </summary>
public static LocalisableString Lounge => new TranslatableString(getKey(@"lounge"), @"lounge");
/// <summary>
/// "quick play"
/// </summary>
public static LocalisableString QuickPlay => new TranslatableString(getKey(@"quick_play"), @"quick play");
/// <summary>
/// "A few important words from your dev team!"
/// </summary>
+20
View File
@@ -29,6 +29,26 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"No! Abort mission");
/// <summary>
/// "Failed to automatically locate an osu!stable installation."
/// </summary>
public static LocalisableString StableDirectoryLocationHeaderText => new TranslatableString(getKey(@"stable_directory_location_header_text"), @"Failed to automatically locate an osu!stable installation.");
/// <summary>
/// "An existing install could not be located. If you know where it is, you can help locate it."
/// </summary>
public static LocalisableString StableDirectoryLocationBodyText => new TranslatableString(getKey(@"stable_directory_location_body_text"), @"An existing install could not be located. If you know where it is, you can help locate it.");
/// <summary>
/// "Sure! I know where it is located!"
/// </summary>
public static LocalisableString StableDirectoryLocationOkButton => new TranslatableString(getKey(@"stable_directory_location_ok_button"), @"Sure! I know where it is located!");
/// <summary>
/// "Actually I don't have osu!stable installed."
/// </summary>
public static LocalisableString StableDirectoryLocationCancelButton => new TranslatableString(getKey(@"stable_directory_location_cancel_button"), @"Actually I don't have osu!stable installed.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -188,6 +188,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString AudioTrack => new TranslatableString(getKey(@"audio_track"), @"Audio Track");
/// <summary>
/// "Custom sample sets"
/// </summary>
public static LocalisableString CustomSampleSets => new TranslatableString(getKey(@"custom_sample_sets"), @"Custom sample sets");
/// <summary>
/// "Click to select a track"
/// </summary>
+5
View File
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString WaveformOpacity => new TranslatableString(getKey(@"waveform_opacity"), @"Waveform opacity");
/// <summary>
/// "Show storyboard"
/// </summary>
public static LocalisableString ShowStoryboard => new TranslatableString(getKey(@"show_storyboard"), @"Show storyboard");
/// <summary>
/// "Show hit markers"
/// </summary>
@@ -229,6 +229,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RandomSkin => new TranslatableString(getKey(@"random_skin"), @"Random skin");
/// <summary>
/// "Previous skin"
/// </summary>
public static LocalisableString PreviousSkin => new TranslatableString(getKey(@"previous_skin"), @"Previous skin");
/// <summary>
/// "Next skin"
/// </summary>
public static LocalisableString NextSkin => new TranslatableString(getKey(@"next_skin"), @"Next skin");
/// <summary>
/// "Pause / resume replay"
/// </summary>
@@ -14,6 +14,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString InputSectionHeader => new TranslatableString(getKey(@"input_section_header"), @"Input");
/// <summary>
/// "Device: {0}"
/// </summary>
public static LocalisableString Device(LocalisableString text) => new TranslatableString(getKey(@"device"), @"Device: {0}", text);
/// <summary>
/// "Global"
/// </summary>
@@ -72,7 +77,8 @@ namespace osu.Game.Localisation
/// <summary>
/// "The binding you&#39;ve selected conflicts with another existing binding."
/// </summary>
public static LocalisableString KeyBindingConflictDetected => new TranslatableString(getKey(@"key_binding_conflict_detected"), @"The binding you've selected conflicts with another existing binding.");
public static LocalisableString KeyBindingConflictDetected =>
new TranslatableString(getKey(@"key_binding_conflict_detected"), @"The binding you've selected conflicts with another existing binding.");
/// <summary>
/// "Keep existing"
+2 -2
View File
@@ -100,9 +100,9 @@ namespace osu.Game.Localisation
public static LocalisableString ModCustomisationSettings => new TranslatableString(getKey(@"mod_customisation_settings"), @"Many mods have customisation settings that drastically change how they function. Click the Customise button in mod select to view settings!");
/// <summary>
/// "Press {0} to switch to a random skin!"
/// "Press {0} to switch to a random skin! You can also use {1} and {2} to cycle through skins."
/// </summary>
public static LocalisableString RandomSkinShortcut(LocalisableString keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin!", keybind);
public static LocalisableString SkinChangeShortcuts(LocalisableString[] keybind) => new TranslatableString(getKey(@"random_skin_shortcut"), @"Press {0} to switch to a random skin! You can also use {1} and {2} to cycle through skins.", keybind[0], keybind[1], keybind[2]);
/// <summary>
/// "While watching a replay, press {0} to toggle replay settings!"
@@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString CurrentSkin => new TranslatableString(getKey(@"current_skin"), @"Current skin");
/// <summary>
/// "Skin name"
/// </summary>
public static LocalisableString SkinName => new TranslatableString(getKey(@"skin_name"), @"Skin name");
/// <summary>
/// "Skin layout editor"
/// </summary>
@@ -264,6 +264,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString TemporarilyShowingAllBeatmapsIn => new TranslatableString(getKey(@"temporarily_showing_all_beatmaps_in"), @"Temporarily showing all beatmaps in");
/// <summary>
/// "mostly {0}"
/// </summary>
public static LocalisableString MostlyBPM(int mostCommonBPM) => new TranslatableString(getKey(@"mostly_bpm"), @"mostly {0}", mostCommonBPM);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+5
View File
@@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString PlaySomeRuleset(string arg0) => new TranslatableString(getKey(@"play_some_ruleset"), @"play some {0}", arg0);
/// <summary>
/// "running"
/// </summary>
public static LocalisableString TimeRunning => new TranslatableString(getKey(@"time_running"), @"running");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -184,6 +184,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString PressForMenu => new TranslatableString(getKey(@"press_for_menu"), @"press for menu");
/// <summary>
/// "Device"
/// </summary>
public static LocalisableString Device => new TranslatableString(getKey(@"device"), @"Device");
/// <summary>
/// "Show hidden"
/// </summary>
public static LocalisableString ShowHidden => new TranslatableString(getKey(@"show_hidden"), @"Show hidden");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+2 -2
View File
@@ -428,10 +428,10 @@ namespace osu.Game.Online.API
// attempt to parse a non-form error message
var response = JObject.Parse(req.GetResponseString().AsNonNull());
string redirect = (string)response.SelectToken(@"url", true);
string redirect = (string)response.SelectToken(@"url", false);
string message = (string)response.SelectToken(@"error", false);
if (!string.IsNullOrEmpty(redirect))
if (!string.IsNullOrEmpty(redirect) || !string.IsNullOrEmpty(message))
{
return new RegistrationRequest.RegistrationRequestErrors
{
+5 -3
View File
@@ -14,9 +14,11 @@ namespace osu.Game.Online.API
protected override void PrePerform()
{
AddParameter("user[username]", Username);
AddParameter("user[user_email]", Email);
AddParameter("user[password]", Password);
AddParameter(@"user[username]", Username);
AddParameter(@"user[user_email]", Email);
AddParameter(@"user[password]", Password);
AddHeader(@"Accept", @"application/json");
base.PrePerform();
}
@@ -221,6 +221,8 @@ namespace osu.Game.Online
{
public MultipleFriendsOnlineNotification(ICollection<APIUser> users)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOnline(string.Join(@", ", users.Select(u => u.Username)));
}
@@ -258,6 +260,8 @@ namespace osu.Game.Online
{
public MultipleFriendsOfflineNotification(ICollection<APIUser> users)
{
Transient = true;
IsImportant = false;
Text = NotificationsStrings.FriendOffline(string.Join(@", ", users.Select(u => u.Username)));
}
@@ -10,7 +10,8 @@ namespace osu.Game.Online.Matchmaking
/// <summary>
/// Retrieves all active matchmaking pools.
/// </summary>
Task<MatchmakingPool[]> GetMatchmakingPools();
/// <param name="type"></param>
Task<MatchmakingPool[]> GetMatchmakingPoolsOfType(MatchmakingPoolType type);
/// <summary>
/// Joins the matchmaking lobby, allowing the local user to receive status updates.
@@ -23,6 +23,9 @@ namespace osu.Game.Online.Matchmaking
[Key(3)]
public string Name { get; set; } = string.Empty;
[Key(4)]
public MatchmakingPoolType Type { get; set; } = MatchmakingPoolType.QuickPlay;
public bool Equals(MatchmakingPool? other)
=> other != null
&& Id == other.Id
@@ -0,0 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Online.Matchmaking
{
public enum MatchmakingPoolType
{
QuickPlay,
RankedPlay
}
}
@@ -4,6 +4,7 @@
using System;
using MessagePack;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
namespace osu.Game.Online.Multiplayer
@@ -16,6 +17,7 @@ namespace osu.Game.Online.Multiplayer
[MessagePackObject]
[Union(0, typeof(TeamVersusRoomState))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
[Union(1, typeof(MatchmakingRoomState))]
[Union(2, typeof(RankedPlayRoomState))]
public abstract class MatchRoomState
{
}
@@ -5,6 +5,7 @@ using System;
using MessagePack;
using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.RankedPlay;
namespace osu.Game.Online.Multiplayer
{
@@ -17,6 +18,7 @@ namespace osu.Game.Online.Multiplayer
[Union(0, typeof(CountdownStartedEvent))]
[Union(1, typeof(CountdownStoppedEvent))]
[Union(2, typeof(MatchmakingAvatarActionEvent))]
[Union(3, typeof(RankedPlayCardHandReplayEvent))]
public abstract class MatchServerEvent
{
}
@@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics.CodeAnalysis;
using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
{
[Serializable]
[MessagePackObject]
public class RankedPlayCardItem : IEquatable<RankedPlayCardItem>
{
/// <summary>
/// A unique identifier for this card.
/// </summary>
[Key(0)]
public Guid ID { get; set; } = Guid.NewGuid();
public bool Equals(RankedPlayCardItem? other)
=> other != null && ID.Equals(other.ID);
public override bool Equals(object? obj)
=> obj is RankedPlayCardItem other && Equals(other);
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override int GetHashCode()
{
return ID.GetHashCode();
}
}
}
@@ -0,0 +1,56 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
{
[Serializable]
[MessagePackObject]
public class RankedPlayDamageInfo : IEquatable<RankedPlayDamageInfo>
{
/// <summary>
/// Total amount of damage dealt.
/// </summary>
[Key(0)]
public required int Damage { get; init; }
/// <summary>
/// Damage dealt before multipliers are applied.
/// </summary>
[Key(1)]
public required int RawDamage { get; init; }
/// <summary>
/// Life before damage was applied.
/// </summary>
[Key(2)]
public required int OldLife { get; init; }
/// <summary>
/// Life after damage was applied.
/// </summary>
[Key(3)]
public required int NewLife { get; init; }
public bool Equals(RankedPlayDamageInfo? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
return Damage == other.Damage && RawDamage == other.RawDamage && OldLife == other.OldLife && NewLife == other.NewLife;
}
public override bool Equals(object? obj)
{
if (obj is null) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((RankedPlayDamageInfo)obj);
}
public override int GetHashCode() => HashCode.Combine(Damage, RawDamage, OldLife, NewLife);
}
}
@@ -0,0 +1,65 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
{
[Serializable]
[MessagePackObject]
public class RankedPlayRoomState : MatchRoomState
{
/// <summary>
/// The current room stage.
/// </summary>
[Key(0)]
public RankedPlayStage Stage { get; set; }
/// <summary>
/// The current round number (1-based).
/// </summary>
[Key(1)]
public int CurrentRound { get; set; }
/// <summary>
/// A multiplier applied to life point damage.
/// </summary>
[Key(2)]
public double DamageMultiplier { get; set; } = 1;
/// <summary>
/// A dictionary containing all users in the room.
/// </summary>
[Key(3)]
public Dictionary<int, RankedPlayUserInfo> Users { get; set; } = [];
/// <summary>
/// The ID of the user currently playing a card.
/// </summary>
[Key(4)]
public int? ActiveUserId { get; set; }
/// <summary>
/// The average star rating of all cards.
/// </summary>
[Key(5)]
public double StarRating { get; set; }
/// <summary>
/// The winner of the match.
/// </summary>
[Key(6)]
public int? WinningUserId { get; set; }
/// <summary>
/// The user currently playing a card.
/// </summary>
[IgnoreMember]
public RankedPlayUserInfo? ActiveUser => ActiveUserId == null ? null : Users[ActiveUserId.Value];
[IgnoreMember]
public RankedPlayUserInfo? WinningUser => WinningUserId == null ? null : Users[WinningUserId.Value];
}
}
@@ -0,0 +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.
namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
{
public enum RankedPlayStage
{
/// <summary>
/// Waiting for clients to join.
/// </summary>
WaitForJoin,
/// <summary>
/// Period of time before the round starts.
/// </summary>
RoundWarmup,
/// <summary>
/// Users are discarding cards and drawing new ones.
/// </summary>
CardDiscard,
/// <summary>
/// Users have finished discarding their cards.
/// </summary>
FinishCardDiscard,
/// <summary>
/// The active user is selecting a card to play.
/// </summary>
CardPlay,
/// <summary>
/// The active user has made a selection, both players should now start downloading it.
/// </summary>
FinishCardPlay,
/// <summary>
/// Period of time before gameplay starts.
/// </summary>
GameplayWarmup,
/// <summary>
/// Gameplay is in progress.
/// </summary>
Gameplay,
/// <summary>
/// Users are viewing the gameplay results
/// </summary>
Results,
/// <summary>
/// The match has concluded.
/// </summary>
Ended
}
}
@@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using MessagePack;
namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
{
[Serializable]
[MessagePackObject]
public class RankedPlayUserInfo
{
/// <summary>
/// This user's matchmaking rating.
/// </summary>
[Key(0)]
public required int Rating { get; set; }
/// <summary>
/// The current life points.
/// </summary>
[Key(1)]
public int Life { get; set; } = 1_000_000;
/// <summary>
/// The cards in this user's hand.
/// </summary>
[Key(2)]
public List<RankedPlayCardItem> Hand { get; set; } = [];
/// <summary>
/// Rating after conclusion of the match.
/// </summary>
[Key(3)]
public int RatingAfter { get; set; }
/// <summary>
/// Information about damage being applied in the current stage.
/// </summary>
/// <remarks>
/// This value is only expected to be populated during the <see cref="RankedPlayStage.Results"/> stage.
/// </remarks>
[Key(4)]
public RankedPlayDamageInfo? DamageInfo;
}
}
@@ -6,6 +6,7 @@ using MessagePack;
using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.RankedPlay;
namespace osu.Game.Online.Multiplayer
{
@@ -19,6 +20,7 @@ namespace osu.Game.Online.Multiplayer
[Union(1, typeof(StartMatchCountdownRequest))]
[Union(2, typeof(StopCountdownRequest))]
[Union(3, typeof(MatchmakingAvatarActionRequest))]
[Union(4, typeof(RankedPlayCardHandReplayRequest))]
public abstract class MatchUserRequest
{
}
@@ -1129,7 +1129,7 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
public abstract Task<MatchmakingPool[]> GetMatchmakingPools();
public abstract Task<MatchmakingPool[]> GetMatchmakingPoolsOfType(MatchmakingPoolType type);
public abstract Task MatchmakingJoinLobby();
@@ -3,11 +3,11 @@
using System;
using System.Diagnostics;
using System.Net.WebSockets;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
using osu.Game.Utils;
namespace osu.Game.Online.Multiplayer
{
@@ -23,12 +23,10 @@ namespace osu.Game.Online.Multiplayer
onError?.Invoke(exception);
if (exception is WebSocketException wse && wse.Message == @"The remote party closed the WebSocket connection without completing the close handshake.")
{
// OnlineStatusNotifier is already letting users know about interruptions to connections.
// Silence these because it gets very spammy otherwise.
// OnlineStatusNotifier is already letting users know about interruptions to connections.
// Silence these because it gets very spammy otherwise.
if (SentryLogger.IsLocalUserConnectivityException(exception))
return;
}
if (exception.GetHubExceptionMessage() is string message)
{
@@ -5,6 +5,7 @@ using System;
using MessagePack;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Multiplayer.Countdown;
using osu.Game.Online.RankedPlay;
namespace osu.Game.Online.Multiplayer
{
@@ -16,6 +17,7 @@ namespace osu.Game.Online.Multiplayer
[Union(1, typeof(ForceGameplayStartCountdown))]
[Union(2, typeof(ServerShuttingDownCountdown))]
[Union(3, typeof(MatchmakingStageCountdown))]
[Union(4, typeof(RankedPlayStageCountdown))]
public abstract class MultiplayerCountdown
{
/// <summary>
@@ -333,13 +333,13 @@ namespace osu.Game.Online.Multiplayer
return connector.Disconnect();
}
public override Task<MatchmakingPool[]> GetMatchmakingPools()
public override Task<MatchmakingPool[]> GetMatchmakingPoolsOfType(MatchmakingPoolType type)
{
if (!IsConnected.Value)
return Task.FromResult(Array.Empty<MatchmakingPool>());
Debug.Assert(connection != null);
return connection.InvokeAsync<MatchmakingPool[]>(nameof(IMatchmakingServer.GetMatchmakingPools));
return connection.InvokeAsync<MatchmakingPool[]>(nameof(IMatchmakingServer.GetMatchmakingPoolsOfType), type);
}
public override Task MatchmakingJoinLobby()
@@ -0,0 +1,39 @@
// 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.Threading.Tasks;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
namespace osu.Game.Online.RankedPlay
{
public interface IRankedPlayClient
{
/// <summary>
/// Indicates that a card has been added to a user's hand.
/// </summary>
/// <param name="userId">The user whose hand has changed.</param>
/// <param name="card">The card added to the user's hand.</param>
Task RankedPlayCardAdded(int userId, RankedPlayCardItem card);
/// <summary>
/// Indicates that a card has been removed from a user's hand.
/// </summary>
/// <param name="userId">The user whose hand has changed.</param>
/// <param name="card">The card removed from the user's hand.</param>
Task RankedPlayCardRemoved(int userId, RankedPlayCardItem card);
/// <summary>
/// Indicates that a card has been revealed to the local user.
/// </summary>
/// <param name="card">The card that was revealed.</param>
/// <param name="item">The playlist item the card corresponds to.</param>
Task RankedPlayCardRevealed(RankedPlayCardItem card, MultiplayerPlaylistItem item);
/// <summary>
/// Indicates a card was played.
/// </summary>
/// <param name="card">The card played.</param>
Task RankedPlayCardPlayed(RankedPlayCardItem card);
}
}
@@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
namespace osu.Game.Online.RankedPlay
{
public interface IRankedPlayServer
{
/// <summary>
/// Discards cards from the local user's hand during the <see cref="RankedPlayStage.CardDiscard"/> stage.
/// </summary>
Task DiscardCards(RankedPlayCardItem[] cards);
/// <summary>
/// Plays a card from the local user's hand during the <see cref="RankedPlayStage.CardPlay"/> stage.
/// Only usable while the local user is the <see cref="RankedPlayRoomState.ActiveUserId">active player</see>.
/// </summary>
Task PlayCard(RankedPlayCardItem card);
}
}
@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using MessagePack;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.RankedPlay
{
[Serializable]
[MessagePackObject]
public class RankedPlayCardHandReplayEvent : MatchServerEvent
{
/// <summary>
/// The user performing the action.
/// </summary>
[Key(0)]
public int UserId { get; set; }
[Key(1)]
public required RankedPlayCardHandReplayFrame[] Frames { get; init; }
}
}

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