mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 04:59:52 +08:00
Compare commits
67 Commits
@@ -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
@@ -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.209.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -176,15 +176,20 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Miss,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SmallTickMiss,
|
||||
HitResult.LargeBonus,
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderDescending().First(v => variant >= v);
|
||||
}
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
@@ -392,9 +392,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
HitResult.Good,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
HitResult.Miss,
|
||||
|
||||
// HitResult.SmallBonus is used for awarding perfect bonus score but is not included here as
|
||||
// it would be a bit redundant to show this to the user.
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.ComboBreak,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -277,19 +277,24 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
HitResult.Miss,
|
||||
|
||||
HitResult.LargeTickHit,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.SmallTickHit,
|
||||
HitResult.SmallTickMiss,
|
||||
HitResult.SliderTailHit,
|
||||
HitResult.SmallBonus,
|
||||
HitResult.LargeBonus,
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -222,15 +222,18 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override RulesetSettingsSubsection CreateSettings() => new TaikoSettingsSubsection(this);
|
||||
|
||||
protected override IEnumerable<HitResult> GetValidHitResults()
|
||||
public override IEnumerable<HitResult> GetValidHitResults()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Ok,
|
||||
HitResult.Miss,
|
||||
|
||||
HitResult.SmallBonus,
|
||||
HitResult.LargeBonus,
|
||||
HitResult.IgnoreHit,
|
||||
HitResult.IgnoreMiss,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -116,6 +117,69 @@ namespace osu.Game.Tests.Database
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFakedRulesetIdIsDetected()
|
||||
{
|
||||
RunTestWithRealm((realm, storage) =>
|
||||
{
|
||||
LoadTestRuleset.HasImplementations = true;
|
||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
||||
|
||||
var ruleset = new LoadTestRuleset();
|
||||
string rulesetShortName = ruleset.RulesetInfo.ShortName;
|
||||
|
||||
realm.Write(r => r.Add(new RulesetInfo(rulesetShortName, ruleset.RulesetInfo.Name, ruleset.RulesetInfo.InstantiationInfo, 0)
|
||||
{
|
||||
Available = true,
|
||||
}));
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
|
||||
|
||||
// Availability is updated on construction of a RealmRulesetStore
|
||||
using var _ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultipleRulesetWithSameOnlineIdsAreDetected()
|
||||
{
|
||||
RunTestWithRealm((realm, storage) =>
|
||||
{
|
||||
LoadTestRuleset.HasImplementations = true;
|
||||
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
|
||||
LoadTestRuleset.OnlineID = 2;
|
||||
|
||||
var first = new LoadTestRuleset();
|
||||
var second = new CatchRuleset();
|
||||
|
||||
realm.Write(r => r.Add(new RulesetInfo(first.ShortName, first.RulesetInfo.Name, first.RulesetInfo.InstantiationInfo, first.RulesetInfo.OnlineID)
|
||||
{
|
||||
Available = true,
|
||||
}));
|
||||
realm.Write(r => r.Add(new RulesetInfo(second.ShortName, second.RulesetInfo.Name, second.RulesetInfo.InstantiationInfo, second.RulesetInfo.OnlineID)
|
||||
{
|
||||
Available = true,
|
||||
}));
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(first.ShortName)!.Available), Is.True);
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(second.ShortName)!.Available), Is.True);
|
||||
|
||||
// Availability is updated on construction of a RealmRulesetStore
|
||||
using var _ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(first.ShortName)!.Available), Is.False);
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(second.ShortName)!.Available), Is.False);
|
||||
|
||||
realm.Write(r => r.Remove(r.Find<RulesetInfo>(first.ShortName)!));
|
||||
|
||||
using var __ = new RealmRulesetStore(realm, storage);
|
||||
|
||||
Assert.That(realm.Run(r => r.Find<RulesetInfo>(second.ShortName)!.Available), Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
private class LoadTestRuleset : Ruleset
|
||||
{
|
||||
public override string RulesetAPIVersionSupported => Version;
|
||||
@@ -124,6 +188,13 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
public static string Version { get; set; } = CURRENT_RULESET_API_VERSION;
|
||||
|
||||
public static int OnlineID { get; set; } = -1;
|
||||
|
||||
public LoadTestRuleset()
|
||||
{
|
||||
RulesetInfo.OnlineID = OnlineID;
|
||||
}
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||
{
|
||||
if (!HasImplementations)
|
||||
|
||||
@@ -526,7 +526,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
// ReSharper disable once MemberHidesStaticFromOuterClass
|
||||
private class TestRuleset : Ruleset
|
||||
{
|
||||
protected override IEnumerable<HitResult> GetValidHitResults() => new[] { HitResult.Great };
|
||||
public override IEnumerable<HitResult> GetValidHitResults() => new[] { HitResult.Great };
|
||||
|
||||
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new NotImplementedException();
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
public partial class TestSceneClickableTeamFlag : OsuManualInputManagerTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetUp()
|
||||
{
|
||||
AddStep("create flags", () =>
|
||||
{
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Spacing = new Vector2(10f),
|
||||
Children = new[]
|
||||
{
|
||||
new ClickableTeamFlag(
|
||||
new APITeam
|
||||
{
|
||||
Id = 1,
|
||||
Name = "Collective Wangs",
|
||||
ShortName = "WANG",
|
||||
}, showTooltipOnHover: false) { Width = 300, Height = 150 },
|
||||
new ClickableTeamFlag(
|
||||
new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = "https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
}, showTooltipOnHover: true) { Width = 300, Height = 150 },
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHover()
|
||||
{
|
||||
AddStep("hover flag with no tooltip", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableTeamFlag>().ElementAt(0)));
|
||||
AddWaitStep("wait", 3);
|
||||
AddAssert("tooltip is not visible", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||
AddStep("hover flag with tooltip", () => InputManager.MoveMouseTo(this.ChildrenOfType<ClickableTeamFlag>().ElementAt(1)));
|
||||
AddUntilStep("wait for tooltip to show", () => this.ChildrenOfType<OsuTooltipContainer.OsuTooltip>().FirstOrDefault()?.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Users.Drawables;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public partial class TestSceneUpdateableTeamFlag : OsuTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestHideOnNull()
|
||||
{
|
||||
UpdateableTeamFlag flag = null!;
|
||||
|
||||
AddStep("create flag with team", () => Child = flag = new UpdateableTeamFlag(createTeam(), hideOnNull: true) { Width = 300, Height = 150 });
|
||||
AddAssert("flag is present", () => flag.IsPresent, () => Is.True);
|
||||
AddStep("set team to null", () => flag.Team = null);
|
||||
AddAssert("flag is not present", () => flag.IsPresent, () => Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void DontHideOnNull()
|
||||
{
|
||||
UpdateableTeamFlag flag = null!;
|
||||
|
||||
AddStep("create flag with team", () => Child = flag = new UpdateableTeamFlag(createTeam(), hideOnNull: false) { Width = 300, Height = 150 });
|
||||
AddAssert("flag is present", () => flag.IsPresent, () => Is.True);
|
||||
AddStep("set team to null", () => flag.Team = null);
|
||||
AddAssert("flag is present", () => flag.IsPresent, () => Is.True);
|
||||
}
|
||||
|
||||
private static APITeam createTeam() => new APITeam
|
||||
{
|
||||
Id = 2,
|
||||
Name = "mom?",
|
||||
ShortName = "MOM",
|
||||
FlagUrl = @"https://assets.ppy.sh/teams/flag/1/b46fb10dbfd8a35dc50e6c00296c0dc6172dffc3ed3d3a4b379277ba498399fe.png",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -90,6 +90,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
AddUntilStep("3 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 3);
|
||||
|
||||
AddStep("select first preset", () => InputManager.Key(Key.Number1));
|
||||
AddAssert("first panel selected", () => this.ChildrenOfType<ModPresetPanel>().ElementAt(0).Active.Value);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ namespace osu.Game.Database
|
||||
|
||||
var beatmap = new Beatmap();
|
||||
|
||||
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
|
||||
HitResult maxRulesetJudgement = ruleset.GetHitResultsForDisplay().First().result;
|
||||
|
||||
// This is a list of all results, ordered from best to worst.
|
||||
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Margin = new MarginPadding { Left = 2 },
|
||||
};
|
||||
|
||||
protected bool DrawBorder { get; init; }
|
||||
protected bool DrawBorder { get; init; } = true;
|
||||
|
||||
private OsuCaret? caret;
|
||||
|
||||
|
||||
@@ -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
-6
@@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -139,5 +139,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
public void SetDefault() => Current.SetDefault();
|
||||
|
||||
public bool IsDisabled => Current.Disabled;
|
||||
|
||||
public float MainDrawHeight => DrawHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,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,
|
||||
|
||||
@@ -631,5 +631,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
public void SetDefault() => Current.SetDefault();
|
||||
|
||||
public bool IsDisabled => Current.Disabled;
|
||||
|
||||
public float MainDrawHeight => DrawHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,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);
|
||||
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,6 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
public string ShortName { get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty(@"flag_url")]
|
||||
public string FlagUrl = string.Empty;
|
||||
public string? FlagUrl = string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -34,5 +34,14 @@ namespace osu.Game.Online.Multiplayer.MatchTypes.RankedPlay
|
||||
/// </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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -55,6 +55,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
[Key(7)]
|
||||
public bool VotedToSkipIntro;
|
||||
|
||||
/// <summary>
|
||||
/// The role of this user in the room.
|
||||
/// </summary>
|
||||
[Key(8)]
|
||||
public MultiplayerRoomUserRole Role;
|
||||
|
||||
[IgnoreMember]
|
||||
public APIUser? User { get; set; }
|
||||
|
||||
|
||||
@@ -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.Multiplayer
|
||||
{
|
||||
public enum MultiplayerRoomUserRole
|
||||
{
|
||||
Player,
|
||||
Referee,
|
||||
}
|
||||
}
|
||||
@@ -10,9 +10,9 @@ namespace osu.Game.Online
|
||||
WebsiteUrl = APIUrl = @"https://osu.ppy.sh";
|
||||
APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk";
|
||||
APIClientID = "5";
|
||||
SpectatorUrl = "https://spectator.ppy.sh/spectator";
|
||||
MultiplayerUrl = "https://spectator.ppy.sh/multiplayer";
|
||||
MetadataUrl = "https://spectator.ppy.sh/metadata";
|
||||
SpectatorUrl = "https://spectator.osu.ppy.sh/spectator";
|
||||
MultiplayerUrl = "https://spectator.osu.ppy.sh/multiplayer";
|
||||
MetadataUrl = "https://spectator.osu.ppy.sh/metadata";
|
||||
BeatmapSubmissionServiceUrl = "https://bss.ppy.sh";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,6 +1585,20 @@ namespace osu.Game
|
||||
|
||||
SkinManager.SelectRandomSkin();
|
||||
return true;
|
||||
|
||||
case GlobalAction.NextSkin:
|
||||
if (skinEditor.State.Value == Visibility.Visible)
|
||||
return false;
|
||||
|
||||
SkinManager.SelectNextSkin();
|
||||
return true;
|
||||
|
||||
case GlobalAction.PreviousSkin:
|
||||
if (skinEditor.State.Value == Visibility.Visible)
|
||||
return false;
|
||||
|
||||
SkinManager.SelectPreviousSkin();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
@@ -209,13 +209,11 @@ namespace osu.Game.Overlays.AccountCreation
|
||||
passwordDescription.AddErrors(errors.User.Password);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(errors.Redirect))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(errors.Message))
|
||||
passwordDescription.AddErrors(new[] { errors.Message });
|
||||
if (!string.IsNullOrEmpty(errors.Message))
|
||||
passwordDescription.AddErrors(new[] { errors.Message });
|
||||
|
||||
if (!string.IsNullOrEmpty(errors.Redirect))
|
||||
game?.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}", LinkWarnMode.NeverWarn);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
|
||||
var ruleset = scores.First().Ruleset.CreateInstance();
|
||||
|
||||
foreach (var resultGroup in ruleset.GetHitResults().GroupBy(r => r.displayName))
|
||||
foreach (var resultGroup in ruleset.GetHitResultsForDisplay().GroupBy(r => r.displayName))
|
||||
{
|
||||
if (!resultGroup.Any(r => allScoreStatistics.Contains(r.result)))
|
||||
continue;
|
||||
|
||||
@@ -234,7 +234,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
public EditorTextBox()
|
||||
{
|
||||
Masking = false;
|
||||
Masking = DrawBorder = false;
|
||||
TextContainer.Height = 0.4f;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
public partial class CurrentlyOnlineDisplay : CompositeDrawable
|
||||
{
|
||||
private Box background = null!;
|
||||
private UserListToolbar userListToolbar = null!;
|
||||
private Container<RealtimeUserList> listContainer = null!;
|
||||
private LoadingLayer loading = null!;
|
||||
private BasicSearchTextBox searchTextBox = null!;
|
||||
|
||||
private CancellationTokenSource? listLoadCancellation;
|
||||
|
||||
public CurrentlyOnlineDisplay()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Margin = new MarginPadding { Bottom = 20 },
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = 40,
|
||||
Vertical = 20
|
||||
},
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 50),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
searchTextBox = new BasicSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Height = 40,
|
||||
ReleaseFocusOnCommit = false,
|
||||
HoldFocus = true,
|
||||
PlaceholderText = HomeStrings.SearchPlaceholder,
|
||||
},
|
||||
Empty(),
|
||||
userListToolbar = new UserListToolbar(false)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
listContainer = new Container<RealtimeUserList>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING }
|
||||
},
|
||||
loading = new LoadingLayer(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
background.Colour = colourProvider.Background4;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
userListToolbar.DisplayStyle.BindValueChanged(_ => reloadList(), true);
|
||||
}
|
||||
|
||||
private void reloadList()
|
||||
{
|
||||
listLoadCancellation?.Cancel();
|
||||
var cancellationSource = listLoadCancellation = new CancellationTokenSource();
|
||||
|
||||
RealtimeUserList? currentList = listContainer.SingleOrDefault();
|
||||
RealtimeUserList newList = new RealtimeUserList(userListToolbar.DisplayStyle.Value)
|
||||
{
|
||||
SortCriteria = { BindTarget = userListToolbar.SortCriteria },
|
||||
SearchText = { BindTarget = searchTextBox.Current }
|
||||
};
|
||||
|
||||
loading.Show();
|
||||
LoadComponentAsync(newList, finishLoad, cancellationSource.Token);
|
||||
|
||||
void finishLoad(RealtimeUserList list)
|
||||
{
|
||||
loading.Hide();
|
||||
|
||||
if (currentList != null)
|
||||
{
|
||||
currentList.FadeOut(250, Easing.OutQuint).Expire();
|
||||
currentList.Delay(25).Schedule(() => currentList.BypassAutoSizeAxes = Axes.Y);
|
||||
}
|
||||
|
||||
listContainer.Add(newList);
|
||||
newList.FadeInFromZero(250, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
listLoadCancellation?.Cancel();
|
||||
listLoadCancellation?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
internal partial class OnlineUserGridPanel : OnlineUserPanel
|
||||
{
|
||||
public OnlineUserGridPanel(APIUser user)
|
||||
: base(user)
|
||||
{
|
||||
Size = new Vector2(290, 162);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new DelayedLoadUnloadWrapper(() =>
|
||||
{
|
||||
var content = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserGridPanel(User)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
},
|
||||
new PurpleRoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Spectate",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = BeginSpectating,
|
||||
Enabled = { BindTarget = CanSpectate },
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
content.OnLoadComplete += _ => content.FadeInFromZero(800, Easing.OutQuint);
|
||||
|
||||
return content;
|
||||
}, 40, 5000)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
public partial class OnlineUserListPanel : OnlineUserPanel
|
||||
{
|
||||
public OnlineUserListPanel(APIUser user)
|
||||
: base(user)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 40;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new DelayedLoadUnloadWrapper(() =>
|
||||
{
|
||||
var content = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new UserListPanel(User),
|
||||
new PurpleRoundedButton
|
||||
{
|
||||
Width = 100,
|
||||
Text = "Spectate",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = BeginSpectating,
|
||||
Enabled = { BindTarget = CanSpectate },
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
content.OnLoadComplete += _ => content.FadeInFromZero(800, Easing.OutQuint);
|
||||
|
||||
return content;
|
||||
}, 40, 5000)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
public abstract partial class OnlineUserPanel : CompositeDrawable, IFilterable
|
||||
{
|
||||
public readonly APIUser User;
|
||||
|
||||
public readonly Bindable<bool> CanSpectate = new Bindable<bool>();
|
||||
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner? performer { get; set; }
|
||||
|
||||
protected OnlineUserPanel(APIUser user)
|
||||
{
|
||||
User = user;
|
||||
FilterTerms = new LocalisableString[] { User.Username };
|
||||
}
|
||||
|
||||
protected void BeginSpectating()
|
||||
{
|
||||
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User)));
|
||||
}
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms { get; }
|
||||
|
||||
public bool FilteringActive { set; get; }
|
||||
|
||||
public bool MatchingFilter
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Show();
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.CurrentlyOnline
|
||||
{
|
||||
internal partial class RealtimeUserList : CompositeDrawable
|
||||
{
|
||||
public readonly IBindable<UserSortCriteria> SortCriteria = new Bindable<UserSortCriteria>();
|
||||
public readonly IBindable<string> SearchText = new Bindable<string>();
|
||||
|
||||
private readonly IBindableDictionary<int, UserPresence> onlineUserPresences = new BindableDictionary<int, UserPresence>();
|
||||
private readonly Dictionary<int, OnlineUserPanel> userPanels = new Dictionary<int, OnlineUserPanel>();
|
||||
private readonly OverlayPanelDisplayStyle style;
|
||||
|
||||
private OnlineUserSearchContainer searchContainer = null!;
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
[Cached(typeof(UserLookupCache))] // not used at the moment.
|
||||
private UserWithRankLookupCache userCache { get; set; } = new UserWithRankLookupCache();
|
||||
|
||||
public RealtimeUserList(OverlayPanelDisplayStyle style)
|
||||
{
|
||||
this.style = style;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(userCache);
|
||||
|
||||
InternalChild = searchContainer = new OnlineUserSearchContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(style == OverlayPanelDisplayStyle.Card ? 10 : 3),
|
||||
SortCriteria = { BindTarget = SortCriteria },
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
onlineUserPresences.BindTo(metadataClient.UserPresences);
|
||||
onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true);
|
||||
|
||||
SearchText.BindValueChanged(onSearchTextChanged, true);
|
||||
|
||||
Scheduler.AddDelayed(updateUsers, 2000, true);
|
||||
}
|
||||
|
||||
private void onSearchTextChanged(ValueChangedEvent<string> search)
|
||||
{
|
||||
searchContainer.SearchTerm = search.NewValue;
|
||||
}
|
||||
|
||||
private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyDictionaryChangedAction.Replace:
|
||||
foreach ((int userId, var presence) in e.NewItems!)
|
||||
{
|
||||
if (userPanels.TryGetValue(userId, out var userPanel))
|
||||
updateUserSpectateState(presence, userPanel);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Add:
|
||||
pendingUsers.AddRange(e.NewItems!.Select(i => i.Key));
|
||||
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Remove:
|
||||
foreach ((int userId, _) in e.OldItems!)
|
||||
{
|
||||
if (userPanels.Remove(userId, out var userPanel))
|
||||
userPanel.Expire();
|
||||
|
||||
pendingUsers.Remove(userId);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly HashSet<int> pendingUsers = new HashSet<int>();
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// ReSharper disable once InconsistentlySynchronizedField
|
||||
if (pendingUsers.Count > 100)
|
||||
updateUsers();
|
||||
}
|
||||
|
||||
private void updateUsers()
|
||||
{
|
||||
if (pendingUsers.Count == 0)
|
||||
return;
|
||||
|
||||
// partitioning here is just to break up the requests.
|
||||
// without this, the intitial request will take seconds to minutes.
|
||||
const int partition_size = 50;
|
||||
|
||||
for (int i = 0; i <= pendingUsers.Count / partition_size; i++)
|
||||
{
|
||||
int[] partitionedUsers = pendingUsers.Skip(i * partition_size).Take(partition_size).ToArray();
|
||||
|
||||
userCache.GetUsersAsync(partitionedUsers).ContinueWith(task => Schedule(() =>
|
||||
{
|
||||
var users = task.GetResultSafely();
|
||||
|
||||
foreach (APIUser? user in users)
|
||||
{
|
||||
if (user == null)
|
||||
continue;
|
||||
|
||||
var presence = metadataClient.GetPresence(user.Id);
|
||||
|
||||
if (presence == null)
|
||||
continue;
|
||||
|
||||
if (userPanels.TryGetValue(user.Id, out _))
|
||||
return;
|
||||
|
||||
// This is quite dodgy – it affects the global `UserLookupCache`.
|
||||
//
|
||||
// but it's the best we can do for now.
|
||||
// this should probaly be returned by server-spectator not osu-web.
|
||||
user.LastVisit = DateTimeOffset.Now;
|
||||
|
||||
var panel = createUserPanel(user);
|
||||
updateUserSpectateState(presence.Value, panel);
|
||||
searchContainer.Add(userPanels[user.Id] = panel);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pendingUsers.Clear();
|
||||
}
|
||||
|
||||
private static void updateUserSpectateState(UserPresence presence, OnlineUserPanel userPanel)
|
||||
{
|
||||
switch (presence.Activity)
|
||||
{
|
||||
default:
|
||||
userPanel.CanSpectate.Value = false;
|
||||
break;
|
||||
|
||||
case UserActivity.InSoloGame:
|
||||
case UserActivity.InMultiplayerGame:
|
||||
case UserActivity.InPlaylistGame:
|
||||
userPanel.CanSpectate.Value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private OnlineUserPanel createUserPanel(APIUser user)
|
||||
{
|
||||
switch (style)
|
||||
{
|
||||
default:
|
||||
case OverlayPanelDisplayStyle.Card:
|
||||
return new OnlineUserGridPanel(user)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
};
|
||||
|
||||
case OverlayPanelDisplayStyle.List:
|
||||
return new OnlineUserListPanel(user);
|
||||
}
|
||||
}
|
||||
|
||||
private partial class OnlineUserSearchContainer : SearchContainer<OnlineUserPanel>
|
||||
{
|
||||
public readonly IBindable<UserSortCriteria> SortCriteria = new Bindable<UserSortCriteria>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
SortCriteria.BindValueChanged(_ => InvalidateLayout(), true);
|
||||
}
|
||||
|
||||
public override IEnumerable<Drawable> FlowingChildren
|
||||
{
|
||||
get
|
||||
{
|
||||
IEnumerable<OnlineUserPanel> panels = base.FlowingChildren.OfType<OnlineUserPanel>();
|
||||
|
||||
switch (SortCriteria.Value)
|
||||
{
|
||||
default:
|
||||
case UserSortCriteria.LastVisit:
|
||||
// Todo: Last visit time is not currently updated according to realtime user presence.
|
||||
return panels.OrderByDescending(panel => panel.User.LastVisit).ThenBy(panel => panel.User.Id);
|
||||
|
||||
case UserSortCriteria.Rank:
|
||||
// Todo: Rank is not currently displayed in the panels. Additionally the sort mode kind of breaks if you change ruleset with this overlay open.
|
||||
return panels.OrderByDescending(panel => panel.User.Rank?.Rank != null).ThenBy(panel => panel.User.Rank?.Rank ?? 0);
|
||||
|
||||
case UserSortCriteria.Username:
|
||||
return panels.OrderBy(panel => panel.User.Username);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is implemented local to avoid invalidating the full cache on ruleset change at a global `UserLookupCache` level.
|
||||
/// We should probably do better than this (server-spectator sending the rank data instead? something else?).
|
||||
/// </summary>
|
||||
private partial class UserWithRankLookupCache : UserLookupCache
|
||||
{
|
||||
[Resolved]
|
||||
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
ruleset.BindValueChanged(ruleset =>
|
||||
{
|
||||
if (ruleset.OldValue?.OnlineID != ruleset.NewValue?.OnlineID)
|
||||
Clear();
|
||||
});
|
||||
}
|
||||
|
||||
protected override LookupUsersRequest CreateRequest(IEnumerable<int> ids) => new LookupUsersRequest(ids.ToArray(), ruleset.Value?.OnlineID >= 0 ? ruleset.Value.OnlineID : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard
|
||||
{
|
||||
internal partial class CurrentlyOnlineDisplay : CompositeDrawable
|
||||
{
|
||||
private const float search_textbox_height = 40;
|
||||
private const float padding = 10;
|
||||
|
||||
private readonly IBindableDictionary<int, UserPresence> onlineUserPresences = new BindableDictionary<int, UserPresence>();
|
||||
private readonly Dictionary<int, OnlineUserPanel> userPanels = new Dictionary<int, OnlineUserPanel>();
|
||||
|
||||
private SearchContainer<OnlineUserPanel> userFlow = null!;
|
||||
private BasicSearchTextBox searchTextBox = null!;
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient metadataClient { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; } = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = padding * 2 + search_textbox_height,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new Container<BasicSearchTextBox>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = padding },
|
||||
Child = searchTextBox = new BasicSearchTextBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Height = search_textbox_height,
|
||||
ReleaseFocusOnCommit = false,
|
||||
HoldFocus = true,
|
||||
PlaceholderText = HomeStrings.SearchPlaceholder,
|
||||
},
|
||||
},
|
||||
userFlow = new SearchContainer<OnlineUserPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(10),
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Top = padding * 3 + search_textbox_height,
|
||||
Bottom = padding,
|
||||
Right = padding,
|
||||
Left = padding,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
searchTextBox.Current.ValueChanged += text => userFlow.SearchTerm = text.NewValue;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
onlineUserPresences.BindTo(metadataClient.UserPresences);
|
||||
onlineUserPresences.BindCollectionChanged(onUserPresenceUpdated, true);
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
searchTextBox.TakeFocus();
|
||||
}
|
||||
|
||||
private void onUserPresenceUpdated(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e) => Schedule(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyDictionaryChangedAction.Add:
|
||||
Debug.Assert(e.NewItems != null);
|
||||
|
||||
foreach (var kvp in e.NewItems)
|
||||
{
|
||||
int userId = kvp.Key;
|
||||
|
||||
users.GetUserAsync(userId).ContinueWith(task =>
|
||||
{
|
||||
if (task.GetResultSafely() is APIUser user)
|
||||
Schedule(() => userFlow.Add(userPanels[userId] = createUserPanel(user)));
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NotifyDictionaryChangedAction.Remove:
|
||||
Debug.Assert(e.OldItems != null);
|
||||
|
||||
foreach (var kvp in e.OldItems)
|
||||
{
|
||||
int userId = kvp.Key;
|
||||
if (userPanels.Remove(userId, out var userPanel))
|
||||
userPanel.Expire();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
private OnlineUserPanel createUserPanel(APIUser user) =>
|
||||
new OnlineUserPanel(user).With(panel =>
|
||||
{
|
||||
panel.Anchor = Anchor.TopCentre;
|
||||
panel.Origin = Anchor.TopCentre;
|
||||
});
|
||||
|
||||
public partial class OnlineUserPanel : CompositeDrawable, IFilterable
|
||||
{
|
||||
public readonly APIUser User;
|
||||
|
||||
private PurpleRoundedButton spectateButton = null!;
|
||||
|
||||
public IEnumerable<LocalisableString> FilterTerms { get; }
|
||||
|
||||
[Resolved]
|
||||
private IPerformFromScreenRunner? performer { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private MetadataClient? metadataClient { get; set; }
|
||||
|
||||
public bool FilteringActive { set; get; }
|
||||
|
||||
public bool MatchingFilter
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value)
|
||||
Show();
|
||||
else
|
||||
Hide();
|
||||
}
|
||||
}
|
||||
|
||||
public OnlineUserPanel(APIUser user)
|
||||
{
|
||||
User = user;
|
||||
|
||||
FilterTerms = new LocalisableString[] { User.Username };
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// TODO: we probably don't want to do this every frame.
|
||||
var activity = metadataClient?.GetPresence(User.Id)?.Activity;
|
||||
|
||||
switch (activity)
|
||||
{
|
||||
default:
|
||||
spectateButton.Enabled.Value = false;
|
||||
break;
|
||||
|
||||
case UserActivity.InSoloGame:
|
||||
case UserActivity.InMultiplayerGame:
|
||||
case UserActivity.InPlaylistGame:
|
||||
case UserActivity.PlayingDailyChallenge:
|
||||
spectateButton.Enabled.Value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2),
|
||||
Width = 290,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new UserGridPanel(User)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
},
|
||||
spectateButton = new PurpleRoundedButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = "Spectate",
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = () => performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User))),
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
PlaceholderText = HomeStrings.SearchPlaceholder,
|
||||
},
|
||||
Empty(),
|
||||
userListToolbar = new UserListToolbar
|
||||
userListToolbar = new UserListToolbar(true)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
@@ -173,7 +173,9 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
listLoadCancellation?.Cancel();
|
||||
var cancellationSource = listLoadCancellation = new CancellationTokenSource();
|
||||
|
||||
FriendsList? currentList = listContainer.SingleOrDefault();
|
||||
// There may be more than one active list in the container due to the delayed fade out.
|
||||
FriendsList? currentList = listContainer.SingleOrDefault(d => d.LifetimeEnd == double.MaxValue);
|
||||
|
||||
FriendsList newList = new FriendsList(userListToolbar.DisplayStyle.Value, apiFriends.Select(f => f.TargetUser!).ToArray())
|
||||
{
|
||||
OnlineStream = { BindTarget = streamControl.Current },
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Overlays.Dashboard.Friends
|
||||
{
|
||||
@@ -14,11 +16,16 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
|
||||
public Bindable<OverlayPanelDisplayStyle> DisplayStyle => styleControl.Current;
|
||||
|
||||
private readonly Bindable<OverlayPanelDisplayStyle> configDisplayStyle = new Bindable<OverlayPanelDisplayStyle>();
|
||||
|
||||
private readonly bool supportsBrickMode;
|
||||
private readonly UserSortTabControl sortControl;
|
||||
private readonly OverlayPanelDisplayStyleControl styleControl;
|
||||
|
||||
public UserListToolbar()
|
||||
public UserListToolbar(bool supportsBrickMode)
|
||||
{
|
||||
this.supportsBrickMode = supportsBrickMode;
|
||||
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
AddInternal(new FillFlowContainer
|
||||
@@ -33,7 +40,7 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
styleControl = new OverlayPanelDisplayStyleControl
|
||||
styleControl = new OverlayPanelDisplayStyleControl(supportsBrickMode)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -41,5 +48,30 @@ namespace osu.Game.Overlays.Dashboard.Friends
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
config.BindWith(OsuSetting.DashboardSortMode, SortCriteria);
|
||||
config.BindWith(OsuSetting.DashboardDisplayStyle, configDisplayStyle);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
configDisplayStyle.BindValueChanged(style =>
|
||||
{
|
||||
if (style.NewValue == OverlayPanelDisplayStyle.Brick && !supportsBrickMode)
|
||||
DisplayStyle.Value = OverlayPanelDisplayStyle.Card;
|
||||
else
|
||||
DisplayStyle.Value = style.NewValue;
|
||||
}, true);
|
||||
|
||||
DisplayStyle.BindValueChanged(style =>
|
||||
{
|
||||
configDisplayStyle.Value = style.NewValue;
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.Metadata;
|
||||
using osu.Game.Overlays.Dashboard;
|
||||
using osu.Game.Overlays.Dashboard.CurrentlyOnline;
|
||||
using osu.Game.Overlays.Dashboard.Friends;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
|
||||
@@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
panels.Add(new ModPresetPanel(preset.ToLive(realm))
|
||||
{
|
||||
Index = i <= 10 ? (i + 1) % 10 : null,
|
||||
Index = i < 10 ? (i + 1) % 10 : null,
|
||||
Shear = Vector2.Zero
|
||||
});
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays
|
||||
|
||||
protected override bool AddEnumEntriesAutomatically => false;
|
||||
|
||||
public OverlayPanelDisplayStyleControl()
|
||||
public OverlayPanelDisplayStyleControl(bool supportsBrickMode)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
@@ -41,10 +41,14 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
Icon = FontAwesome.Solid.Bars
|
||||
});
|
||||
AddTabItem(new PanelDisplayTabItem(OverlayPanelDisplayStyle.Brick)
|
||||
|
||||
if (supportsBrickMode)
|
||||
{
|
||||
Icon = FontAwesome.Solid.Th
|
||||
});
|
||||
AddTabItem(new PanelDisplayTabItem(OverlayPanelDisplayStyle.Brick)
|
||||
{
|
||||
Icon = FontAwesome.Solid.Th
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
|
||||
|
||||
@@ -142,10 +142,17 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
/// <param name="triggerKey">The key which triggered this update, and should be used as the binding.</param>
|
||||
public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey)
|
||||
{
|
||||
var combination = fullState.Keys.Where(KeyCombination.IsModifierKey)
|
||||
.Append(triggerKey)
|
||||
.Select(k => k.GetVirtualKey() ?? k)
|
||||
.ToArray();
|
||||
var keys = fullState.Keys
|
||||
.Where(KeyCombination.IsModifierKey)
|
||||
.Append(triggerKey)
|
||||
.ToArray();
|
||||
|
||||
// For gameplay bindings, users care about being able to use both left / right shift as different bindings.
|
||||
// For global bindings, it's better to combine both of these into a virtual key which covers both side modifiers.
|
||||
var combination = KeyBinding.Value.RulesetName == null
|
||||
? keys.Select(k => k.GetVirtualKey() ?? k).ToArray()
|
||||
: keys;
|
||||
|
||||
UpdateKeyCombination(new KeyCombination(combination));
|
||||
}
|
||||
|
||||
|
||||
@@ -136,6 +136,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Action = RestoreDefaults,
|
||||
},
|
||||
new Container
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Screens;
|
||||
|
||||
@@ -17,20 +18,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
public StableDirectoryLocationDialog(TaskCompletionSource<string> taskCompletionSource)
|
||||
{
|
||||
HeaderText = "Failed to automatically locate an osu!stable installation.";
|
||||
BodyText = "An existing install could not be located. If you know where it is, you can help locate it.";
|
||||
HeaderText = DialogStrings.StableDirectoryLocationHeaderText;
|
||||
BodyText = DialogStrings.StableDirectoryLocationBodyText;
|
||||
Icon = FontAwesome.Solid.QuestionCircle;
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = "Sure! I know where it is located!",
|
||||
Text = DialogStrings.StableDirectoryLocationOkButton,
|
||||
Action = () => Schedule(() => performer.PerformFromScreen(screen => screen.Push(new StableDirectorySelectScreen(taskCompletionSource))))
|
||||
},
|
||||
new PopupDialogCancelButton
|
||||
{
|
||||
Text = "Actually I don't have osu!stable installed.",
|
||||
Text = DialogStrings.StableDirectoryLocationCancelButton,
|
||||
Action = () => taskCompletionSource.TrySetCanceled()
|
||||
}
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
@@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
|
||||
protected override bool IsValidDirectory(DirectoryInfo? info) => legacyImportManager.IsUsableForStableImport(info, out _);
|
||||
|
||||
public override LocalisableString HeaderText => "Please select your osu!stable install location";
|
||||
public override LocalisableString HeaderText => MaintenanceSettingsStrings.StableDirectorySelectHeader;
|
||||
|
||||
public StableDirectorySelectScreen(TaskCompletionSource<string> taskCompletionSource)
|
||||
{
|
||||
|
||||
@@ -44,12 +44,6 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
|
||||
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "skins" });
|
||||
|
||||
private static readonly Live<SkinInfo> random_skin_info = new SkinInfo
|
||||
{
|
||||
ID = SkinInfo.RANDOM_SKIN,
|
||||
Name = "<Random Skin>",
|
||||
}.ToLiveUnmanaged();
|
||||
|
||||
private readonly List<Live<SkinInfo>> dropdownItems = new List<Live<SkinInfo>>();
|
||||
|
||||
[Resolved]
|
||||
@@ -104,7 +98,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
|
||||
skinDropdown.Current.BindValueChanged(skin =>
|
||||
{
|
||||
if (skin.NewValue == random_skin_info)
|
||||
if (skin.NewValue.ID == SkinInfo.RANDOM_SKIN)
|
||||
{
|
||||
// before selecting random, set the skin back to the previous selection.
|
||||
// this is done because at this point it will be random_skin_info, and would
|
||||
@@ -121,21 +115,9 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
// Because we are using `Live<>` in this class, we don't need to worry about this scenario too much.
|
||||
if (!sender.Any())
|
||||
return;
|
||||
|
||||
// For simplicity repopulate the full list.
|
||||
// In the future we should change this to properly handle ChangeSet events.
|
||||
dropdownItems.Clear();
|
||||
|
||||
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_SKIN).ToLive(realm));
|
||||
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.ARGON_PRO_SKIN).ToLive(realm));
|
||||
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.TRIANGLES_SKIN).ToLive(realm));
|
||||
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.CLASSIC_SKIN).ToLive(realm));
|
||||
dropdownItems.Add(sender.Single(s => s.ID == SkinInfo.RETRO_SKIN).ToLive(realm));
|
||||
|
||||
dropdownItems.Add(random_skin_info);
|
||||
|
||||
foreach (var skin in sender.Where(s => !s.Protected))
|
||||
dropdownItems.Add(skin.ToLive(realm));
|
||||
dropdownItems.AddRange(skins.GetAllUsableSkins());
|
||||
|
||||
Schedule(() => skinDropdown.Items = dropdownItems);
|
||||
}
|
||||
@@ -279,7 +261,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
{
|
||||
textBox = new FocusedTextBox
|
||||
{
|
||||
PlaceholderText = @"Skin name",
|
||||
PlaceholderText = SkinSettingsStrings.SkinName,
|
||||
FontSize = OsuFont.DEFAULT_FONT_SIZE,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
SelectAllOnFocus = true,
|
||||
|
||||
@@ -85,12 +85,6 @@ namespace osu.Game.Overlays.Settings
|
||||
controlDefault.BindValueChanged(_ => updateDefaultState());
|
||||
controlEnabled.BindValueChanged(_ => updateDefaultState(), true);
|
||||
FinishTransforms(true);
|
||||
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
revertButton.RelativeSizeAxes = Axes.None;
|
||||
revertButton.Height = ((Drawable)Control).DrawHeight;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateDefaultState()
|
||||
@@ -108,6 +102,8 @@ namespace osu.Game.Overlays.Settings
|
||||
base.Update();
|
||||
controlDefault.Value = Control.IsDefault;
|
||||
controlEnabled.Value = !Control.IsDisabled;
|
||||
|
||||
revertButton.Height = Control.MainDrawHeight;
|
||||
}
|
||||
|
||||
#region ISettingsItem
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public SettingsRevertToDefaultButton()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = WIDTH;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Toolbar
|
||||
@@ -76,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "running",
|
||||
Text = ToolbarStrings.TimeRunning,
|
||||
Font = OsuFont.Default.With(size: 10, weight: FontWeight.SemiBold),
|
||||
},
|
||||
gameTime = new OsuSpriteText
|
||||
|
||||
@@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Volume
|
||||
@@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Volume
|
||||
[Resolved]
|
||||
private VolumeOverlay volumeOverlay { get; set; } = null!;
|
||||
|
||||
public MasterVolumeMeter(string name, float circleSize, Color4 meterColour)
|
||||
public MasterVolumeMeter(LocalisableString name, float circleSize, Color4 meterColour)
|
||||
: base(name, circleSize, meterColour)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
@@ -42,7 +43,7 @@ namespace osu.Game.Overlays.Volume
|
||||
protected readonly float CircleSize;
|
||||
|
||||
private readonly Color4 meterColour;
|
||||
private readonly string name;
|
||||
private readonly LocalisableString name;
|
||||
|
||||
private OsuSpriteText text;
|
||||
private BufferedContainer maxGlow;
|
||||
@@ -75,7 +76,7 @@ namespace osu.Game.Overlays.Volume
|
||||
|
||||
private const float transition_length = 500;
|
||||
|
||||
public VolumeMeter(string name, float circleSize, Color4 meterColour)
|
||||
public VolumeMeter(LocalisableString name, float circleSize, Color4 meterColour)
|
||||
{
|
||||
CircleSize = circleSize;
|
||||
this.meterColour = meterColour;
|
||||
|
||||
@@ -6,6 +6,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -15,6 +16,7 @@ using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Volume;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@@ -67,9 +69,9 @@ namespace osu.Game.Overlays
|
||||
Spacing = new Vector2(0, offset),
|
||||
Children = new[]
|
||||
{
|
||||
volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker),
|
||||
volumeMeterMaster = new MasterVolumeMeter("MASTER", 150, colours.PinkDarker) { IsMuted = { BindTarget = IsMuted }, },
|
||||
volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker),
|
||||
volumeMeterEffect = new VolumeMeter(AudioSettingsStrings.EffectVolume.ToUpper(), 125, colours.BlueDarker),
|
||||
volumeMeterMaster = new MasterVolumeMeter(AudioSettingsStrings.MasterVolume.ToUpper(), 150, colours.PinkDarker) { IsMuted = { BindTarget = IsMuted }, },
|
||||
volumeMeterMusic = new VolumeMeter(AudioSettingsStrings.MusicVolume.ToUpper(), 125, colours.BlueDarker),
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsPercentageSlider<double>))]
|
||||
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(MinimumAccuracySlider))]
|
||||
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
|
||||
{
|
||||
MinValue = 0.60,
|
||||
@@ -103,4 +103,12 @@ namespace osu.Game.Rulesets.Mods
|
||||
Standard,
|
||||
}
|
||||
}
|
||||
|
||||
public partial class MinimumAccuracySlider : SettingsPercentageSlider<double>
|
||||
{
|
||||
public MinimumAccuracySlider()
|
||||
{
|
||||
KeyboardStep = 0.01f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public partial class MuteComboSlider : RoundedSliderBar<int>
|
||||
{
|
||||
public MuteComboSlider()
|
||||
{
|
||||
KeyboardStep = 1;
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText => FormatMuteComboValue(Current.Value);
|
||||
|
||||
public static LocalisableString FormatMuteComboValue(int value)
|
||||
|
||||
@@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public partial class HiddenComboSlider : RoundedSliderBar<int>
|
||||
{
|
||||
public HiddenComboSlider()
|
||||
{
|
||||
KeyboardStep = 1;
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText => FormatHiddenComboValue(Current.Value);
|
||||
|
||||
public static LocalisableString FormatHiddenComboValue(int value)
|
||||
|
||||
@@ -93,6 +93,12 @@ namespace osu.Game.Rulesets
|
||||
$"Ruleset API version is too old (was {instance.RulesetAPIVersionSupported}, expected {Ruleset.CURRENT_RULESET_API_VERSION})");
|
||||
}
|
||||
|
||||
if (r.OnlineID != instanceInfo.OnlineID)
|
||||
throw new InvalidOperationException($@"Online ID mismatch for ruleset {r.ShortName}: database has {r.OnlineID}, constructed instance has {instanceInfo.OnlineID}");
|
||||
|
||||
if (r.OnlineID > 0 && rulesets.Any(otherRuleset => otherRuleset.ShortName != r.ShortName && otherRuleset.OnlineID == r.OnlineID))
|
||||
throw new InvalidOperationException($@"Ruleset {r.ShortName} shares online ID {r.OnlineID} with another ruleset");
|
||||
|
||||
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
|
||||
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
|
||||
resolvedType.Assembly.GetTypes();
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Configuration;
|
||||
@@ -334,13 +335,17 @@ namespace osu.Game.Rulesets
|
||||
public virtual StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticItem>();
|
||||
|
||||
/// <summary>
|
||||
/// Get all valid <see cref="HitResult"/>s for this ruleset.
|
||||
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
|
||||
/// Get all <see cref="HitResult"/>s for this ruleset which are important enough to displayed to the end user.
|
||||
/// Used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are not returned by this method.
|
||||
/// Values are returned as ordered by <see cref="OrderAttribute"/>.
|
||||
/// </remarks>
|
||||
/// <returns>
|
||||
/// All valid <see cref="HitResult"/>s along with a display-friendly name.
|
||||
/// All relevant <see cref="HitResult"/>s along with a display-friendly name.
|
||||
/// </returns>
|
||||
public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResults()
|
||||
public IEnumerable<(HitResult result, LocalisableString displayName)> GetHitResultsForDisplay()
|
||||
{
|
||||
var validResults = GetValidHitResults();
|
||||
|
||||
@@ -353,6 +358,7 @@ namespace osu.Game.Rulesets
|
||||
case HitResult.None:
|
||||
case HitResult.IgnoreHit:
|
||||
case HitResult.IgnoreMiss:
|
||||
case HitResult.ComboBreak:
|
||||
// display is handled as a completion count with corresponding "hit" type.
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.SmallTickMiss:
|
||||
@@ -366,12 +372,10 @@ namespace osu.Game.Rulesets
|
||||
|
||||
/// <summary>
|
||||
/// Get all valid <see cref="HitResult"/>s for this ruleset.
|
||||
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
|
||||
/// Used for strict validation purposes. The ruleset should return ALL applicable <see cref="HitResult"/> types here
|
||||
/// (except <see cref="HitResult.None"/> and obsolete types).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are ignored even when specified.
|
||||
/// </remarks>
|
||||
protected virtual IEnumerable<HitResult> GetValidHitResults() => EnumExtensions.GetValuesInOrder<HitResult>();
|
||||
public virtual IEnumerable<HitResult> GetValidHitResults() => EnumExtensions.GetValuesInOrder<HitResult>();
|
||||
|
||||
/// <summary>
|
||||
/// Get a display friendly name for the specified result type.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user