mirror of
https://github.com/ppy/osu.git
synced 2025-03-15 15:27:20 +08:00
Merge branch 'fix-gameplay-leaderboard-layout' into gameplay-leaderboards
This commit is contained in:
commit
29c4d06d36
@ -52,7 +52,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.908.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.916.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
});
|
||||
}
|
||||
|
||||
private class TestSkin : DefaultSkin
|
||||
private class TestSkin : TrianglesSkin
|
||||
{
|
||||
public bool FlipCatcherPlate { get; set; }
|
||||
|
||||
|
@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(SkinManager skins)
|
||||
{
|
||||
var defaultLegacySkin = skins.DefaultLegacySkin;
|
||||
var defaultLegacySkin = skins.DefaultClassicSkin;
|
||||
|
||||
// sprite names intentionally swapped to match stable member naming / ease of cross-referencing
|
||||
explosion1.Texture = defaultLegacySkin.GetTexture("scoreboard-explosion-2");
|
||||
|
@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
[Test]
|
||||
public void TestDefaultSkin()
|
||||
{
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = DefaultSkin.CreateInfo().ToLiveUnmanaged());
|
||||
AddStep("set default skin", () => skins.CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -125,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
/// Ensures alternation is reset before the first hitobject after a break.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
|
||||
public void TestInputSingularWithBreak([Values] bool pressBeforeSecondObject) => CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModAlternate(),
|
||||
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
|
||||
@ -155,21 +156,26 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
},
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
ReplayFrames = new ReplayFrame[]
|
||||
{
|
||||
// first press to start alternate lock.
|
||||
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(501, new Vector2(100)),
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
new OsuReplayFrame(450, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(451, new Vector2(100)),
|
||||
// press same key at second hitobject and ensure it has been hit.
|
||||
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2501, new Vector2(500, 100)),
|
||||
new OsuReplayFrame(2450, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2451, new Vector2(500, 100)),
|
||||
// press same key at third hitobject and ensure it has been missed.
|
||||
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(3001, new Vector2(500, 100)),
|
||||
}
|
||||
new OsuReplayFrame(2950, new Vector2(500, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2951, new Vector2(500, 100)),
|
||||
}.Concat(!pressBeforeSecondObject
|
||||
? Enumerable.Empty<ReplayFrame>()
|
||||
: new ReplayFrame[]
|
||||
{
|
||||
// press same key after break but before hit object.
|
||||
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2251, new Vector2(300, 100)),
|
||||
}
|
||||
).ToList()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
});
|
||||
AddStep("setup default legacy skin", () =>
|
||||
{
|
||||
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
|
||||
skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
Child = new SkinProvidingContainer(new DefaultSkin(null))
|
||||
Child = new SkinProvidingContainer(new TrianglesSkin(null))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = drawableHitCircle = new DrawableHitCircle(hitCircle)
|
||||
|
@ -44,6 +44,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
aimRating = Math.Pow(aimRating, 0.8);
|
||||
flashlightRating = Math.Pow(flashlightRating, 0.8);
|
||||
}
|
||||
|
||||
if (mods.Any(h => h is OsuModRelax))
|
||||
{
|
||||
aimRating *= 0.9;
|
||||
@ -127,6 +133,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||
{
|
||||
new OsuModTouchDevice(),
|
||||
new OsuModDoubleTime(),
|
||||
new OsuModHalfTime(),
|
||||
new OsuModEasy(),
|
||||
|
@ -88,12 +88,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double rawAim = attributes.AimDifficulty;
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModTouchDevice))
|
||||
rawAim = Math.Pow(rawAim, 0.8);
|
||||
|
||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
double aimValue = Math.Pow(5.0 * Math.Max(1.0, attributes.AimDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@ -233,12 +228,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (!score.Mods.Any(h => h is OsuModFlashlight))
|
||||
return 0.0;
|
||||
|
||||
double rawFlashlight = attributes.FlashlightDifficulty;
|
||||
|
||||
if (score.Mods.Any(m => m is OsuModTouchDevice))
|
||||
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
|
||||
|
||||
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
|
||||
double flashlightValue = Math.Pow(attributes.FlashlightDifficulty, 2.0) * 25.0;
|
||||
|
||||
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
|
||||
if (effectiveMissCount > 0)
|
||||
|
@ -18,7 +18,7 @@ using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IUpdatableByPlayfield
|
||||
{
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
|
||||
@ -62,15 +62,18 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
gameplayClock = drawableRuleset.FrameStableClock;
|
||||
}
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
LastAcceptedAction = null;
|
||||
}
|
||||
|
||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||
|
||||
private bool checkCorrectAction(OsuAction action)
|
||||
{
|
||||
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
{
|
||||
LastAcceptedAction = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (action)
|
||||
{
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@ -21,32 +20,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
public const int SPACING = 32;
|
||||
public const double PREEMPT = 800;
|
||||
|
||||
public DrawablePool<FollowPoint> Pool;
|
||||
public DrawablePool<FollowPoint>? Pool { private get; set; }
|
||||
|
||||
protected override void OnApply(FollowPointLifetimeEntry entry)
|
||||
{
|
||||
base.OnApply(entry);
|
||||
|
||||
entry.Invalidated += onEntryInvalidated;
|
||||
refreshPoints();
|
||||
entry.Invalidated += scheduleRefresh;
|
||||
|
||||
// Our clock may not be correct at this point if `LoadComplete` has not run yet.
|
||||
// Without a schedule, animations referencing FollowPoint's clock (see `IAnimationTimeReference`) would be incorrect on first pool usage.
|
||||
scheduleRefresh();
|
||||
}
|
||||
|
||||
protected override void OnFree(FollowPointLifetimeEntry entry)
|
||||
{
|
||||
base.OnFree(entry);
|
||||
|
||||
entry.Invalidated -= onEntryInvalidated;
|
||||
entry.Invalidated -= scheduleRefresh;
|
||||
// Return points to the pool.
|
||||
ClearInternal(false);
|
||||
}
|
||||
|
||||
private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
|
||||
|
||||
private void refreshPoints()
|
||||
private void scheduleRefresh() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
Debug.Assert(Pool != null);
|
||||
|
||||
ClearInternal(false);
|
||||
|
||||
var entry = Entry;
|
||||
|
||||
if (entry?.End == null) return;
|
||||
|
||||
OsuHitObject start = entry.Start;
|
||||
@ -95,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
|
||||
}
|
||||
|
||||
entry.LifetimeEnd = finalTransformEndTime;
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Computes the fade time of follow point positioned between two hitobjects.
|
||||
|
@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
skinManager.CurrentSkinInfo.Value.PerformRead(s =>
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
|
||||
Assert.AreEqual(typeof(TrianglesSkin), s.CreateInstance(skinManager).GetType());
|
||||
|
||||
new LegacySkinExporter(osu.Dependencies.Get<Storage>()).ExportModelTo(s, exportStream);
|
||||
|
||||
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
{
|
||||
Assert.IsFalse(s.Protected);
|
||||
Assert.AreNotEqual(originalSkinId, s.ID);
|
||||
Assert.AreEqual(typeof(DefaultSkin), s.CreateInstance(skinManager).GetType());
|
||||
Assert.AreEqual(typeof(TrianglesSkin), s.CreateInstance(skinManager).GetType());
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
@ -226,7 +226,7 @@ namespace osu.Game.Tests.Skins.IO
|
||||
{
|
||||
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||
|
||||
skinManager.CurrentSkinInfo.Value = skinManager.DefaultLegacySkin.SkinInfo;
|
||||
skinManager.CurrentSkinInfo.Value = skinManager.DefaultClassicSkin.SkinInfo;
|
||||
|
||||
skinManager.EnsureMutableSkin();
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
@ -66,6 +67,18 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("Scroll container is loaded", () => scrollContainer.LoadState >= LoadState.Loaded);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestInitialZoomOutOfRange()
|
||||
{
|
||||
AddStep("Invalid ZoomableScrollContainer throws ArgumentException", () =>
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
_ = new ZoomableScrollContainer(1, 60, 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestWidthInitialization()
|
||||
{
|
||||
|
@ -1,12 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -19,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
SetContents(skin =>
|
||||
{
|
||||
var implementation = skin != null
|
||||
var implementation = skin is LegacySkin
|
||||
? CreateLegacyImplementation()
|
||||
: CreateDefaultImplementation();
|
||||
|
||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||
{
|
||||
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||
CreateSkinTest(TrianglesSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||
}
|
||||
|
@ -6,7 +6,9 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.PolygonExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
@ -18,37 +20,52 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[TestFixture]
|
||||
public class TestSceneGameplayLeaderboard : OsuTestScene
|
||||
{
|
||||
private readonly TestGameplayLeaderboard leaderboard;
|
||||
private TestGameplayLeaderboard leaderboard;
|
||||
|
||||
private readonly BindableDouble playerScore = new BindableDouble();
|
||||
|
||||
public TestSceneGameplayLeaderboard()
|
||||
{
|
||||
Add(leaderboard = new TestGameplayLeaderboard
|
||||
AddStep("toggle expanded", () =>
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
if (leaderboard != null)
|
||||
leaderboard.Expanded.Value = !leaderboard.Expanded.Value;
|
||||
});
|
||||
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestLayoutWithManyScores()
|
||||
{
|
||||
AddStep("reset leaderboard", () =>
|
||||
createLeaderboard();
|
||||
|
||||
AddStep("add many scores in one go", () =>
|
||||
{
|
||||
leaderboard.Clear();
|
||||
playerScore.Value = 1222333;
|
||||
for (int i = 0; i < 32; i++)
|
||||
createRandomScore(new APIUser { Username = $"Player {i + 1}" });
|
||||
|
||||
// Add player at end to force an animation down the whole list.
|
||||
playerScore.Value = 0;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
|
||||
AddStep("add local player", () => createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true));
|
||||
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
|
||||
AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v);
|
||||
// Gameplay leaderboard has custom scroll logic, which when coupled with LayoutDuration
|
||||
// has caused layout to not work in the past.
|
||||
|
||||
AddUntilStep("wait for fill flow layout",
|
||||
() => leaderboard.ChildrenOfType<FillFlowContainer<GameplayLeaderboardScore>>().First().ScreenSpaceDrawQuad.Intersects(leaderboard.ScreenSpaceDrawQuad));
|
||||
|
||||
AddUntilStep("wait for some scores not masked away",
|
||||
() => leaderboard.ChildrenOfType<GameplayLeaderboardScore>().Any(s => leaderboard.ScreenSpaceDrawQuad.Contains(s.ScreenSpaceDrawQuad.Centre)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPlayerScore()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
var player2Score = new BindableDouble(1234567);
|
||||
var player3Score = new BindableDouble(1111111);
|
||||
|
||||
@ -73,6 +90,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestRandomScores()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
int playerNumber = 1;
|
||||
AddRepeatStep("add player with random score", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 10);
|
||||
}
|
||||
@ -80,6 +100,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestExistingUsers()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
AddStep("add peppy", () => createRandomScore(new APIUser { Username = "peppy", Id = 2 }));
|
||||
AddStep("add smoogipoo", () => createRandomScore(new APIUser { Username = "smoogipoo", Id = 1040328 }));
|
||||
AddStep("add flyte", () => createRandomScore(new APIUser { Username = "flyte", Id = 3103765 }));
|
||||
@ -89,6 +112,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestMaxHeight()
|
||||
{
|
||||
createLeaderboard();
|
||||
addLocalPlayer();
|
||||
|
||||
int playerNumber = 1;
|
||||
AddRepeatStep("add 3 other players", () => createRandomScore(new APIUser { Username = $"Player {playerNumber++}" }), 3);
|
||||
checkHeight(4);
|
||||
@ -103,6 +129,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
=> AddAssert($"leaderboard height is {panelCount} panels high", () => leaderboard.DrawHeight == (GameplayLeaderboardScore.PANEL_HEIGHT + leaderboard.Spacing) * panelCount);
|
||||
}
|
||||
|
||||
private void addLocalPlayer()
|
||||
{
|
||||
AddStep("add local player", () =>
|
||||
{
|
||||
playerScore.Value = 1222333;
|
||||
createLeaderboardScore(playerScore, new APIUser { Username = "You", Id = 3 }, true);
|
||||
});
|
||||
}
|
||||
|
||||
private void createLeaderboard()
|
||||
{
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
Child = leaderboard = new TestGameplayLeaderboard
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(2),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private void createRandomScore(APIUser user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user);
|
||||
|
||||
private void createLeaderboardScore(BindableDouble score, APIUser user, bool isTracked = false)
|
||||
|
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
|
||||
private TestParticleSpewer createSpewer() =>
|
||||
new TestParticleSpewer(skinManager.DefaultLegacySkin.GetTexture("star2"))
|
||||
new TestParticleSpewer(skinManager.DefaultClassicSkin.GetTexture("star2"))
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
RelativePositionAxes = Axes.Both,
|
||||
|
@ -264,13 +264,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestMutedNotificationMasterVolume()
|
||||
{
|
||||
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault);
|
||||
addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.Value == 0.5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedNotificationTrackVolume()
|
||||
{
|
||||
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault);
|
||||
addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.Value == 0.5);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -15,8 +15,10 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -101,6 +103,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModReferenceNotRetained()
|
||||
{
|
||||
AddStep("allow fail", () => allowFail = false);
|
||||
|
||||
Mod[] originalMods = { new OsuModDaycore { SpeedChange = { Value = 0.8 } } };
|
||||
Mod[] playerMods = null!;
|
||||
|
||||
AddStep("load player with mods", () => LoadPlayer(originalMods));
|
||||
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
|
||||
|
||||
AddStep("get mods at start of gameplay", () => playerMods = Player.Score.ScoreInfo.Mods.ToArray());
|
||||
|
||||
// Player creates new instance of mods during load.
|
||||
AddAssert("player score has copied mods", () => playerMods.First(), () => Is.Not.SameAs(originalMods.First()));
|
||||
AddAssert("player score has matching mods", () => playerMods.First(), () => Is.EqualTo(originalMods.First()));
|
||||
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
|
||||
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
|
||||
|
||||
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
|
||||
|
||||
// Player creates new instance of mods after gameplay to ensure any runtime references to drawables etc. are not retained.
|
||||
AddAssert("results screen score has copied mods", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.Not.SameAs(playerMods.First()));
|
||||
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||
|
||||
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
|
||||
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreStoredLocally()
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestEditDefaultSkin()
|
||||
{
|
||||
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.DEFAULT_SKIN);
|
||||
AddAssert("is default skin", () => skinManager.CurrentSkinInfo.Value.ID == SkinInfo.TRIANGLES_SKIN);
|
||||
|
||||
AddStep("open settings", () => { Game.Settings.Show(); });
|
||||
|
||||
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("open skin editor", () => skinEditor.Show());
|
||||
|
||||
// Until step required as the skin editor may take time to load (and an extra scheduled frame for the mutable part).
|
||||
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.DEFAULT_SKIN);
|
||||
AddUntilStep("is modified default skin", () => skinManager.CurrentSkinInfo.Value.ID != SkinInfo.TRIANGLES_SKIN);
|
||||
AddAssert("is not protected", () => skinManager.CurrentSkinInfo.Value.PerformRead(s => !s.Protected));
|
||||
|
||||
AddUntilStep("export button enabled", () => Game.Settings.ChildrenOfType<SkinSection.ExportSkinButton>().SingleOrDefault()?.Enabled.Value == true);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -10,6 +11,7 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Updater;
|
||||
@ -32,6 +34,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Vector2.Zero);
|
||||
|
||||
TimeToCompleteProgress = 2000;
|
||||
progressingNotifications.Clear();
|
||||
|
||||
@ -45,7 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
displayedCount = new OsuSpriteText()
|
||||
};
|
||||
|
||||
notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };
|
||||
notificationOverlay.UnreadCount.ValueChanged += count => { displayedCount.Text = $"unread count: {count.NewValue}"; };
|
||||
});
|
||||
|
||||
[Test]
|
||||
@ -228,6 +232,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("wait overlay not present", () => !notificationOverlay.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestProgressClick()
|
||||
{
|
||||
ProgressNotification notification = null!;
|
||||
|
||||
AddStep("add progress notification", () =>
|
||||
{
|
||||
notification = new ProgressNotification
|
||||
{
|
||||
Text = @"Uploading to BSS...",
|
||||
CompletionText = "Uploaded to BSS!",
|
||||
};
|
||||
notificationOverlay.Post(notification);
|
||||
progressingNotifications.Add(notification);
|
||||
});
|
||||
|
||||
AddStep("hover over notification", () => InputManager.MoveMouseTo(notificationOverlay.ChildrenOfType<ProgressNotification>().Single()));
|
||||
|
||||
AddStep("left click", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("not cancelled", () => notification.State == ProgressNotificationState.Active);
|
||||
|
||||
AddStep("right click", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("cancelled", () => notification.State == ProgressNotificationState.Cancelled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCompleteProgress()
|
||||
{
|
||||
@ -270,6 +299,8 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddUntilStep("wait completion", () => notification.State == ProgressNotificationState.Completed);
|
||||
|
||||
AddAssert("Completion toast shown", () => notificationOverlay.ToastCount == 1);
|
||||
AddUntilStep("wait forwarded", () => notificationOverlay.ToastCount == 0);
|
||||
AddAssert("only one unread", () => notificationOverlay.UnreadCount.Value == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -297,7 +328,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
SimpleNotification notification = null!;
|
||||
AddStep(@"post", () => notificationOverlay.Post(notification = new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }));
|
||||
AddUntilStep("check is toast", () => !notification.IsInToastTray);
|
||||
AddUntilStep("check is toast", () => notification.IsInToastTray);
|
||||
AddAssert("light is not visible", () => notification.ChildrenOfType<Notification.NotificationLight>().Single().Alpha == 0);
|
||||
|
||||
AddUntilStep("wait for forward to overlay", () => !notification.IsInToastTray);
|
||||
@ -422,6 +453,14 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddRepeatStep("send barrage", sendBarrage, 10);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestServerShuttingDownNotification()
|
||||
{
|
||||
AddStep("post with 5 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(5))));
|
||||
AddStep("post with 30 seconds", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromSeconds(30))));
|
||||
AddStep("post with 6 hours", () => notificationOverlay.Post(new ServerShutdownNotification(TimeSpan.FromHours(6))));
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
@ -39,7 +39,7 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
// UI/selection defaults
|
||||
SetDefault(OsuSetting.Ruleset, string.Empty);
|
||||
SetDefault(OsuSetting.Skin, SkinInfo.DEFAULT_SKIN.ToString());
|
||||
SetDefault(OsuSetting.Skin, SkinInfo.TRIANGLES_SKIN.ToString());
|
||||
|
||||
SetDefault(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details);
|
||||
SetDefault(OsuSetting.BeatmapDetailModsFilter, false);
|
||||
|
@ -51,7 +51,15 @@ namespace osu.Game.Database
|
||||
ID = id;
|
||||
}
|
||||
|
||||
public bool Equals(Live<T>? other) => ID == other?.ID;
|
||||
public bool Equals(Live<T>? other)
|
||||
{
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
if (other == null) return false;
|
||||
|
||||
return ID == other.ID;
|
||||
}
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(ID);
|
||||
|
||||
public override string ToString() => PerformRead(i => i.ToString());
|
||||
}
|
||||
|
@ -69,8 +69,9 @@ namespace osu.Game.Database
|
||||
/// 22 2022-07-31 Added ModPreset.
|
||||
/// 23 2022-08-01 Added LastLocalUpdate to BeatmapInfo.
|
||||
/// 24 2022-08-22 Added MaximumStatistics to ScoreInfo.
|
||||
/// 25 2022-09-18 Remove skins to add with new naming.
|
||||
/// </summary>
|
||||
private const int schema_version = 24;
|
||||
private const int schema_version = 25;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@ -870,6 +871,11 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 25:
|
||||
// Remove the default skins so they can be added back by SkinManager with updated naming.
|
||||
migration.NewRealm.RemoveRange(migration.NewRealm.All<SkinInfo>().Where(s => s.Protected));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,9 @@ using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Online.API.Requests.Responses
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an aggregate score for a user based off all beatmaps that have been played in the playlist.
|
||||
/// </summary>
|
||||
public class APIUserScoreAggregate
|
||||
{
|
||||
[JsonProperty("attempts")]
|
||||
|
@ -15,6 +15,8 @@ using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
@ -24,6 +26,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
private FillFlowContainer<HitResultCell> topScoreStatistics = null!;
|
||||
private FillFlowContainer<HitResultCell> bottomScoreStatistics = null!;
|
||||
private FillFlowContainer<ModCell> modStatistics = null!;
|
||||
private readonly Bindable<bool> prefer24HourTime = new Bindable<bool>();
|
||||
|
||||
public LeaderboardScoreTooltip()
|
||||
{
|
||||
@ -36,8 +39,9 @@ namespace osu.Game.Online.Leaderboards
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, OsuConfigManager configManager)
|
||||
{
|
||||
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
@ -92,6 +96,13 @@ namespace osu.Game.Online.Leaderboards
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
prefer24HourTime.BindValueChanged(_ => updateTimestampLabel(), true);
|
||||
}
|
||||
|
||||
private ScoreInfo? displayedScore;
|
||||
|
||||
public void SetContent(ScoreInfo score)
|
||||
@ -101,7 +112,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
displayedScore = score;
|
||||
|
||||
timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
|
||||
updateTimestampLabel();
|
||||
|
||||
modStatistics.Clear();
|
||||
topScoreStatistics.Clear();
|
||||
@ -121,6 +132,16 @@ namespace osu.Game.Online.Leaderboards
|
||||
}
|
||||
}
|
||||
|
||||
private void updateTimestampLabel()
|
||||
{
|
||||
if (displayedScore != null)
|
||||
{
|
||||
timestampLabel.Text = prefer24HourTime.Value
|
||||
? $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy HH:mm}"
|
||||
: $"Played on {displayedScore.Date.ToLocalTime():d MMMM yyyy h:mm tt}";
|
||||
}
|
||||
}
|
||||
|
||||
protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
|
||||
protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
|
||||
|
||||
|
@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Rooms.RoomStatuses;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Utils;
|
||||
@ -26,6 +27,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public abstract class MultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer
|
||||
{
|
||||
public Action<Notification>? PostNotification { protected get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when any change occurs to the multiplayer room.
|
||||
/// </summary>
|
||||
@ -207,6 +210,8 @@ namespace osu.Game.Online.Multiplayer
|
||||
|
||||
updateLocalRoomSettings(joinedRoom.Settings);
|
||||
|
||||
postServerShuttingDownNotification();
|
||||
|
||||
OnRoomJoined();
|
||||
}, cancellationSource.Token).ConfigureAwait(false);
|
||||
}, cancellationSource.Token).ConfigureAwait(false);
|
||||
@ -554,6 +559,14 @@ namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
case CountdownStartedEvent countdownStartedEvent:
|
||||
Room.ActiveCountdowns.Add(countdownStartedEvent.Countdown);
|
||||
|
||||
switch (countdownStartedEvent.Countdown)
|
||||
{
|
||||
case ServerShuttingDownCountdown:
|
||||
postServerShuttingDownNotification();
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case CountdownStoppedEvent countdownStoppedEvent:
|
||||
@ -569,6 +582,16 @@ namespace osu.Game.Online.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void postServerShuttingDownNotification()
|
||||
{
|
||||
ServerShuttingDownCountdown? countdown = room?.ActiveCountdowns.OfType<ServerShuttingDownCountdown>().FirstOrDefault();
|
||||
|
||||
if (countdown == null)
|
||||
return;
|
||||
|
||||
PostNotification?.Invoke(new ServerShutdownNotification(countdown.TimeRemaining));
|
||||
}
|
||||
|
||||
Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability)
|
||||
{
|
||||
Scheduler.Add(() =>
|
||||
|
@ -13,6 +13,7 @@ namespace osu.Game.Online.Multiplayer
|
||||
[MessagePackObject]
|
||||
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||
[Union(1, typeof(ForceGameplayStartCountdown))]
|
||||
[Union(2, typeof(ServerShuttingDownCountdown))]
|
||||
public abstract class MultiplayerCountdown
|
||||
{
|
||||
/// <summary>
|
||||
|
62
osu.Game/Online/Multiplayer/ServerShutdownNotification.cs
Normal file
62
osu.Game/Online/Multiplayer/ServerShutdownNotification.cs
Normal file
@ -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 System;
|
||||
using Humanizer.Localisation;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
public class ServerShutdownNotification : SimpleNotification
|
||||
{
|
||||
private readonly DateTimeOffset endDate;
|
||||
private ScheduledDelegate? updateDelegate;
|
||||
|
||||
public ServerShutdownNotification(TimeSpan duration)
|
||||
{
|
||||
endDate = DateTimeOffset.UtcNow + duration;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
updateTime();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
updateDelegate = Scheduler.Add(updateTimeWithReschedule);
|
||||
}
|
||||
|
||||
private void updateTimeWithReschedule()
|
||||
{
|
||||
updateTime();
|
||||
|
||||
// The remaining time on a countdown may be at a fractional portion between two seconds.
|
||||
// We want to align certain audio/visual cues to the point at which integer seconds change.
|
||||
// To do so, we schedule to the next whole second. Note that scheduler invocation isn't
|
||||
// guaranteed to be accurate, so this may still occur slightly late, but even in such a case
|
||||
// the next invocation will be roughly correct.
|
||||
double timeToNextSecond = endDate.Subtract(DateTimeOffset.UtcNow).TotalMilliseconds % 1000;
|
||||
|
||||
updateDelegate = Scheduler.AddDelayed(updateTimeWithReschedule, timeToNextSecond);
|
||||
}
|
||||
|
||||
private void updateTime()
|
||||
{
|
||||
TimeSpan remaining = endDate.Subtract(DateTimeOffset.Now);
|
||||
|
||||
if (remaining.TotalSeconds <= 5)
|
||||
{
|
||||
updateDelegate?.Cancel();
|
||||
Text = "The multiplayer server will be right back...";
|
||||
}
|
||||
else
|
||||
Text = $"The multiplayer server is restarting in {HumanizerUtils.Humanize(remaining, precision: 3, minUnit: TimeUnit.Second)}.";
|
||||
}
|
||||
}
|
||||
}
|
15
osu.Game/Online/Multiplayer/ServerShuttingDownCountdown.cs
Normal file
15
osu.Game/Online/Multiplayer/ServerShuttingDownCountdown.cs
Normal file
@ -0,0 +1,15 @@
|
||||
// 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 MessagePack;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
/// <summary>
|
||||
/// A countdown that indicates the current multiplayer server is shutting down.
|
||||
/// </summary>
|
||||
[MessagePackObject]
|
||||
public class ServerShuttingDownCountdown : MultiplayerCountdown
|
||||
{
|
||||
}
|
||||
}
|
@ -28,7 +28,8 @@ namespace osu.Game.Online
|
||||
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
||||
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
||||
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown)),
|
||||
(typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown))
|
||||
(typeof(ForceGameplayStartCountdown), typeof(MultiplayerCountdown)),
|
||||
(typeof(ServerShuttingDownCountdown), typeof(MultiplayerCountdown)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -56,14 +56,12 @@ using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osu.Game.Updater;
|
||||
using osu.Game.Users;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Graphics;
|
||||
using Sentry;
|
||||
using Logger = osu.Framework.Logging.Logger;
|
||||
|
||||
namespace osu.Game
|
||||
{
|
||||
@ -294,25 +292,13 @@ namespace osu.Game
|
||||
|
||||
Ruleset.ValueChanged += r => configRuleset.Value = r.NewValue.ShortName;
|
||||
|
||||
// bind config int to database SkinInfo
|
||||
configSkin = LocalConfig.GetBindable<string>(OsuSetting.Skin);
|
||||
|
||||
// Transfer skin from config to realm instance once on startup.
|
||||
SkinManager.SetSkinFromConfiguration(configSkin.Value);
|
||||
|
||||
// Transfer any runtime changes back to configuration file.
|
||||
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
|
||||
configSkin.ValueChanged += skinId =>
|
||||
{
|
||||
Live<SkinInfo> skinInfo = null;
|
||||
|
||||
if (Guid.TryParse(skinId.NewValue, out var guid))
|
||||
skinInfo = SkinManager.Query(s => s.ID == guid);
|
||||
|
||||
if (skinInfo == null)
|
||||
{
|
||||
if (guid == SkinInfo.CLASSIC_SKIN)
|
||||
skinInfo = DefaultLegacySkin.CreateInfo().ToLiveUnmanaged();
|
||||
}
|
||||
|
||||
SkinManager.CurrentSkinInfo.Value = skinInfo ?? DefaultSkin.CreateInfo().ToLiveUnmanaged();
|
||||
};
|
||||
configSkin.TriggerChange();
|
||||
|
||||
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
|
||||
|
||||
@ -730,6 +716,8 @@ namespace osu.Game
|
||||
ScoreManager.PostNotification = n => Notifications.Post(n);
|
||||
ScoreManager.PresentImport = items => PresentScore(items.First().Value);
|
||||
|
||||
MultiplayerClient.PostNotification = n => Notifications.Post(n);
|
||||
|
||||
// make config aware of how to lookup skins for on-screen display purposes.
|
||||
// if this becomes a more common thing, tracked settings should be reconsidered to allow local DI.
|
||||
LocalConfig.LookupSkinName = id => SkinManager.Query(s => s.ID == id)?.ToString() ?? "Unknown";
|
||||
|
@ -179,7 +179,7 @@ namespace osu.Game
|
||||
|
||||
private SpectatorClient spectatorClient;
|
||||
|
||||
private MultiplayerClient multiplayerClient;
|
||||
protected MultiplayerClient MultiplayerClient { get; private set; }
|
||||
|
||||
private MetadataClient metadataClient;
|
||||
|
||||
@ -284,7 +284,7 @@ namespace osu.Game
|
||||
// TODO: OsuGame or OsuGameBase?
|
||||
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
|
||||
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
|
||||
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
|
||||
dependencies.CacheAs(MultiplayerClient = new OnlineMultiplayerClient(endpoints));
|
||||
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
||||
|
||||
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
|
||||
@ -329,7 +329,7 @@ namespace osu.Game
|
||||
AddInternal(apiAccess);
|
||||
|
||||
AddInternal(spectatorClient);
|
||||
AddInternal(multiplayerClient);
|
||||
AddInternal(MultiplayerClient);
|
||||
AddInternal(metadataClient);
|
||||
|
||||
AddInternal(rulesetConfigCache);
|
||||
|
@ -210,14 +210,14 @@ namespace osu.Game.Overlays
|
||||
mainContent.FadeTo(0, TRANSITION_LENGTH, Easing.OutQuint);
|
||||
}
|
||||
|
||||
private void notificationClosed()
|
||||
private void notificationClosed() => Schedule(() =>
|
||||
{
|
||||
updateCounts();
|
||||
|
||||
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
|
||||
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
|
||||
playDebouncedSample("UI/overlay-pop-out");
|
||||
}
|
||||
});
|
||||
|
||||
private void playDebouncedSample(string sampleName)
|
||||
{
|
||||
|
@ -26,7 +26,8 @@ namespace osu.Game.Overlays.Notifications
|
||||
public abstract class Notification : Container
|
||||
{
|
||||
/// <summary>
|
||||
/// User requested close.
|
||||
/// Notification was closed, either by user or otherwise.
|
||||
/// Importantly, this event may be fired from a non-update thread.
|
||||
/// </summary>
|
||||
public event Action? Closed;
|
||||
|
||||
@ -55,6 +56,8 @@ namespace osu.Game.Overlays.Notifications
|
||||
|
||||
protected Container IconContent;
|
||||
|
||||
public bool WasClosed { get; private set; }
|
||||
|
||||
private readonly Container content;
|
||||
|
||||
protected override Container<Drawable> Content => content;
|
||||
@ -226,8 +229,8 @@ namespace osu.Game.Overlays.Notifications
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
// Clicking with anything but left button should dismiss but not perform the activation action.
|
||||
if (e.Button == MouseButton.Left)
|
||||
Activated?.Invoke();
|
||||
if (e.Button == MouseButton.Left && Activated?.Invoke() == false)
|
||||
return true;
|
||||
|
||||
Close(false);
|
||||
return true;
|
||||
@ -245,21 +248,23 @@ namespace osu.Game.Overlays.Notifications
|
||||
initialFlash.FadeOutFromOne(2000, Easing.OutQuart);
|
||||
}
|
||||
|
||||
public bool WasClosed;
|
||||
|
||||
public virtual void Close(bool runFlingAnimation)
|
||||
{
|
||||
if (WasClosed) return;
|
||||
|
||||
WasClosed = true;
|
||||
|
||||
if (runFlingAnimation && dragContainer.FlingLeft())
|
||||
this.FadeOut(600, Easing.In);
|
||||
else
|
||||
this.FadeOut(100);
|
||||
|
||||
Closed?.Invoke();
|
||||
Expire();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
if (runFlingAnimation && dragContainer.FlingLeft())
|
||||
this.FadeOut(600, Easing.In);
|
||||
else
|
||||
this.FadeOut(100);
|
||||
|
||||
Expire();
|
||||
});
|
||||
}
|
||||
|
||||
private class DragContainer : Container
|
||||
|
@ -142,7 +142,6 @@ namespace osu.Game.Overlays.Notifications
|
||||
case ProgressNotificationState.Completed:
|
||||
loadingSpinner.Hide();
|
||||
attemptPostCompletion();
|
||||
base.Close(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -166,6 +165,8 @@ namespace osu.Game.Overlays.Notifications
|
||||
|
||||
CompletionTarget.Invoke(CreateCompletionNotification());
|
||||
completionSent = true;
|
||||
|
||||
Close(false);
|
||||
}
|
||||
|
||||
private ProgressNotificationState state;
|
||||
@ -239,6 +240,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
{
|
||||
switch (State)
|
||||
{
|
||||
case ProgressNotificationState.Completed:
|
||||
case ProgressNotificationState.Cancelled:
|
||||
base.Close(runFlingAnimation);
|
||||
break;
|
||||
|
@ -14,7 +14,6 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
@ -36,9 +35,6 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
Icon = FontAwesome.Solid.PaintBrush
|
||||
};
|
||||
|
||||
private readonly Bindable<Live<SkinInfo>> dropdownBindable = new Bindable<Live<SkinInfo>> { Default = DefaultSkin.CreateInfo().ToLiveUnmanaged() };
|
||||
private readonly Bindable<string> configBindable = new Bindable<string>();
|
||||
|
||||
private static readonly Live<SkinInfo> random_skin_info = new SkinInfo
|
||||
{
|
||||
ID = SkinInfo.RANDOM_SKIN,
|
||||
@ -56,13 +52,14 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
private IDisposable realmSubscription;
|
||||
|
||||
[BackgroundDependencyLoader(permitNulls: true)]
|
||||
private void load(OsuConfigManager config, [CanBeNull] SkinEditorOverlay skinEditor)
|
||||
private void load([CanBeNull] SkinEditorOverlay skinEditor)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
skinDropdown = new SkinSettingsDropdown
|
||||
{
|
||||
LabelText = SkinSettingsStrings.CurrentSkin,
|
||||
Current = skins.CurrentSkinInfo,
|
||||
Keywords = new[] { @"skins" }
|
||||
},
|
||||
new SettingsButton
|
||||
@ -73,47 +70,27 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new ExportSkinButton(),
|
||||
new DeleteSkinButton(),
|
||||
};
|
||||
|
||||
config.BindWith(OsuSetting.Skin, configBindable);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
skinDropdown.Current = dropdownBindable;
|
||||
|
||||
realmSubscription = realm.RegisterForNotifications(_ => realm.Realm.All<SkinInfo>()
|
||||
.Where(s => !s.DeletePending)
|
||||
.OrderByDescending(s => s.Protected) // protected skins should be at the top.
|
||||
.ThenBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged);
|
||||
.OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase), skinsChanged);
|
||||
|
||||
configBindable.BindValueChanged(_ => Scheduler.AddOnce(updateSelectedSkinFromConfig));
|
||||
|
||||
dropdownBindable.BindValueChanged(dropdownSelectionChanged);
|
||||
}
|
||||
|
||||
private void dropdownSelectionChanged(ValueChangedEvent<Live<SkinInfo>> skin)
|
||||
{
|
||||
// Only handle cases where it's clear the user has intent to change skins.
|
||||
if (skin.OldValue == null) return;
|
||||
|
||||
if (skin.NewValue.Equals(random_skin_info))
|
||||
skinDropdown.Current.BindValueChanged(skin =>
|
||||
{
|
||||
var skinBefore = skins.CurrentSkinInfo.Value;
|
||||
|
||||
skins.SelectRandomSkin();
|
||||
|
||||
if (skinBefore == skins.CurrentSkinInfo.Value)
|
||||
if (skin.NewValue == random_skin_info)
|
||||
{
|
||||
// the random selection didn't change the skin, so we should manually update the dropdown to match.
|
||||
dropdownBindable.Value = skins.CurrentSkinInfo.Value;
|
||||
// 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
|
||||
// cause SelectRandomSkin to be unable to skip the previous selection.
|
||||
skins.CurrentSkinInfo.Value = skin.OldValue;
|
||||
skins.SelectRandomSkin();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
configBindable.Value = skin.NewValue.ID.ToString();
|
||||
});
|
||||
}
|
||||
|
||||
private void skinsChanged(IRealmCollection<SkinInfo> sender, ChangeSet changes, Exception error)
|
||||
@ -123,34 +100,19 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
if (!sender.Any())
|
||||
return;
|
||||
|
||||
int protectedCount = sender.Count(s => s.Protected);
|
||||
|
||||
// For simplicity repopulate the full list.
|
||||
// In the future we should change this to properly handle ChangeSet events.
|
||||
dropdownItems.Clear();
|
||||
foreach (var skin in sender)
|
||||
|
||||
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(random_skin_info);
|
||||
|
||||
foreach (var skin in sender.Where(s => !s.Protected))
|
||||
dropdownItems.Add(skin.ToLive(realm));
|
||||
dropdownItems.Insert(protectedCount, random_skin_info);
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
skinDropdown.Items = dropdownItems;
|
||||
|
||||
updateSelectedSkinFromConfig();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSelectedSkinFromConfig()
|
||||
{
|
||||
if (!skinDropdown.Items.Any())
|
||||
return;
|
||||
|
||||
Live<SkinInfo> skin = null;
|
||||
|
||||
if (Guid.TryParse(configBindable.Value, out var configId))
|
||||
skin = skinDropdown.Items.FirstOrDefault(s => s.ID == configId);
|
||||
|
||||
dropdownBindable.Value = skin ?? skinDropdown.Items.First();
|
||||
Schedule(() => skinDropdown.Items = dropdownItems);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
|
@ -150,9 +150,9 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
if (comboBasedSize)
|
||||
{
|
||||
if (combo > 200)
|
||||
if (combo >= 200)
|
||||
size *= 0.8f;
|
||||
else if (combo > 100)
|
||||
else if (combo >= 100)
|
||||
size *= 0.9f;
|
||||
}
|
||||
|
||||
|
@ -137,6 +137,11 @@ namespace osu.Game.Scoring
|
||||
|
||||
clone.Statistics = new Dictionary<HitResult, int>(clone.Statistics);
|
||||
clone.MaximumStatistics = new Dictionary<HitResult, int>(clone.MaximumStatistics);
|
||||
|
||||
// Ensure we have fresh mods to avoid any references (ie. after gameplay).
|
||||
clone.clearAllMods();
|
||||
clone.ModsJson = ModsJson;
|
||||
|
||||
clone.RealmUser = new RealmUser
|
||||
{
|
||||
OnlineID = RealmUser.OnlineID,
|
||||
|
@ -130,7 +130,7 @@ namespace osu.Game.Screens.Backgrounds
|
||||
|
||||
case BackgroundSource.Skin:
|
||||
// default skins should use the default background rotation, which won't be the case if a SkinBackground is created for them.
|
||||
if (skin.Value is DefaultSkin || skin.Value is DefaultLegacySkin)
|
||||
if (skin.Value is TrianglesSkin || skin.Value is DefaultLegacySkin)
|
||||
break;
|
||||
|
||||
newBackground = new SkinBackground(skin.Value, getBackgroundTextureName());
|
||||
|
@ -196,7 +196,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
defaultTimelineZoom = getZoomLevelForVisibleMilliseconds(6000);
|
||||
|
||||
float initialZoom = (float)(defaultTimelineZoom * editorBeatmap.BeatmapInfo.TimelineZoom);
|
||||
float initialZoom = (float)(defaultTimelineZoom * (editorBeatmap.BeatmapInfo.TimelineZoom == 0 ? 1 : editorBeatmap.BeatmapInfo.TimelineZoom));
|
||||
float minimumZoom = getZoomLevelForVisibleMilliseconds(10000);
|
||||
float maximumZoom = getZoomLevelForVisibleMilliseconds(500);
|
||||
|
||||
|
@ -87,6 +87,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
if (minimum > maximum)
|
||||
throw new ArgumentException($"{nameof(minimum)} ({minimum}) must be less than {nameof(maximum)} ({maximum})");
|
||||
|
||||
if (initial < minimum || initial > maximum)
|
||||
throw new ArgumentException($"{nameof(initial)} ({initial}) must be between {nameof(minimum)} ({minimum}) and {nameof(maximum)} ({maximum})");
|
||||
|
||||
minZoom = minimum;
|
||||
maxZoom = maximum;
|
||||
CurrentZoom = zoomTarget = initial;
|
||||
|
@ -110,6 +110,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
if (Client != null)
|
||||
{
|
||||
Client.RoomUpdated -= invokeOnRoomUpdated;
|
||||
Client.LoadRequested -= invokeOnRoomLoadRequested;
|
||||
Client.UserLeft -= invokeUserLeft;
|
||||
Client.UserKicked -= invokeUserKicked;
|
||||
Client.UserJoined -= invokeUserJoined;
|
||||
|
@ -1,10 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -25,9 +22,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
{
|
||||
private const double fade_time = 50;
|
||||
|
||||
private SpriteIcon icon;
|
||||
private OsuSpriteText text;
|
||||
private ProgressBar progressBar;
|
||||
private SpriteIcon icon = null!;
|
||||
private OsuSpriteText text = null!;
|
||||
private ProgressBar progressBar = null!;
|
||||
|
||||
public StateDisplay()
|
||||
{
|
||||
@ -86,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
};
|
||||
}
|
||||
|
||||
private OsuColour colours;
|
||||
private OsuColour colours = null!;
|
||||
|
||||
public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
|
||||
{
|
||||
@ -164,10 +161,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
||||
break;
|
||||
|
||||
case DownloadState.Downloading:
|
||||
Debug.Assert(availability.DownloadProgress != null);
|
||||
|
||||
progressBar.FadeIn(fade_time);
|
||||
progressBar.CurrentTime = availability.DownloadProgress.Value;
|
||||
progressBar.CurrentTime = availability.DownloadProgress ?? 0;
|
||||
|
||||
text.Text = "downloading map";
|
||||
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
|
||||
|
@ -92,6 +92,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
int displayCount = Math.Min(Flow.Count, max_panels);
|
||||
Height = displayCount * (GameplayLeaderboardScore.PANEL_HEIGHT + Flow.Spacing.Y);
|
||||
Flow.Margin = new MarginPadding { Bottom = Height };
|
||||
requiresScroll = displayCount != Flow.Count;
|
||||
|
||||
return drawable;
|
||||
@ -114,7 +115,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (requiresScroll && trackedScore != null)
|
||||
{
|
||||
float scrollTarget = scroll.GetChildPosInContent(trackedScore) + trackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
||||
scroll.ScrollTo(scrollTarget, false);
|
||||
scroll.ScrollTo(scrollTarget);
|
||||
}
|
||||
|
||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||
|
@ -47,10 +47,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (clock != null)
|
||||
gameplayClock = clock;
|
||||
|
||||
// Lock height so changes in text autosize (if character height changes)
|
||||
// don't cause parent invalidation.
|
||||
Height = 14;
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
|
@ -502,7 +502,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private int restartCount;
|
||||
|
||||
private const double volume_requirement = 0.05;
|
||||
private const double volume_requirement = 0.01;
|
||||
|
||||
private void showMuteWarningIfNeeded()
|
||||
{
|
||||
@ -539,10 +539,11 @@ namespace osu.Game.Screens.Play
|
||||
volumeOverlay.IsMuted.Value = false;
|
||||
|
||||
// Check values before resetting, as the user may have only had mute enabled, in which case we might not need to adjust volumes.
|
||||
// Note that we only restore halfway to ensure the user isn't suddenly overloaded by unexpectedly high volume.
|
||||
if (audioManager.Volume.Value <= volume_requirement)
|
||||
audioManager.Volume.SetDefault();
|
||||
audioManager.Volume.Value = 0.5f;
|
||||
if (audioManager.VolumeTrack.Value <= volume_requirement)
|
||||
audioManager.VolumeTrack.SetDefault();
|
||||
audioManager.VolumeTrack.Value = 0.5f;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
@ -15,7 +15,6 @@ using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Framework.Threading;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
@ -24,11 +23,6 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
private BufferedContainer<Column> columns;
|
||||
|
||||
public SquareGraph()
|
||||
{
|
||||
AddLayout(layout);
|
||||
}
|
||||
|
||||
public int ColumnCount => columns?.Children.Count ?? 0;
|
||||
|
||||
private int progress;
|
||||
@ -57,7 +51,7 @@ namespace osu.Game.Screens.Play
|
||||
if (value == values) return;
|
||||
|
||||
values = value;
|
||||
layout.Invalidate();
|
||||
graphNeedsUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,21 +69,25 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
private readonly LayoutValue layout = new LayoutValue(Invalidation.DrawSize);
|
||||
private ScheduledDelegate scheduledCreate;
|
||||
|
||||
private bool graphNeedsUpdate;
|
||||
|
||||
private Vector2 previousDrawSize;
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (values != null && !layout.IsValid)
|
||||
if (graphNeedsUpdate || (values != null && DrawSize != previousDrawSize))
|
||||
{
|
||||
columns?.FadeOut(500, Easing.OutQuint).Expire();
|
||||
|
||||
scheduledCreate?.Cancel();
|
||||
scheduledCreate = Scheduler.AddDelayed(RecreateGraph, 500);
|
||||
|
||||
layout.Validate();
|
||||
previousDrawSize = DrawSize;
|
||||
graphNeedsUpdate = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@ namespace osu.Game.Skinning
|
||||
public static SkinInfo CreateInfo() => new SkinInfo
|
||||
{
|
||||
ID = Skinning.SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon.
|
||||
Name = "osu!classic",
|
||||
Name = "osu! \"classic\" (2013)",
|
||||
Creator = "team osu!",
|
||||
Protected = true,
|
||||
InstantiationInfo = typeof(DefaultLegacySkin).GetInvariantInstantiationInfo()
|
||||
|
@ -81,7 +81,7 @@ namespace osu.Game.Skinning
|
||||
}
|
||||
}
|
||||
|
||||
int lastDefaultSkinIndex = sources.IndexOf(sources.OfType<DefaultSkin>().LastOrDefault());
|
||||
int lastDefaultSkinIndex = sources.IndexOf(sources.OfType<TrianglesSkin>().LastOrDefault());
|
||||
|
||||
// Ruleset resources should be given the ability to override game-wide defaults
|
||||
// This is achieved by placing them before the last instance of DefaultSkin.
|
||||
|
@ -232,6 +232,9 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
skin.SkinInfo.PerformWrite(s =>
|
||||
{
|
||||
// Update for safety
|
||||
s.InstantiationInfo = skin.GetType().GetInvariantInstantiationInfo();
|
||||
|
||||
// Serialise out the SkinInfo itself.
|
||||
string skinInfoJson = JsonConvert.SerializeObject(s, new JsonSerializerSettings { Formatting = Formatting.Indented });
|
||||
|
||||
|
@ -5,7 +5,6 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO;
|
||||
@ -19,7 +18,7 @@ namespace osu.Game.Skinning
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable<SkinInfo>, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles
|
||||
{
|
||||
internal static readonly Guid DEFAULT_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
|
||||
internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD");
|
||||
internal static readonly Guid CLASSIC_SKIN = new Guid("81F02CD3-EEC6-4865-AC23-FAE26A386187");
|
||||
internal static readonly Guid RANDOM_SKIN = new Guid("D39DFEFB-477C-4372-B1EA-2BCEA5FB8908");
|
||||
|
||||
@ -45,7 +44,16 @@ namespace osu.Game.Skinning
|
||||
var type = string.IsNullOrEmpty(InstantiationInfo)
|
||||
// handle the case of skins imported before InstantiationInfo was added.
|
||||
? typeof(LegacySkin)
|
||||
: Type.GetType(InstantiationInfo).AsNonNull();
|
||||
: Type.GetType(InstantiationInfo);
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
// Since the class was renamed from "DefaultSkin" to "TrianglesSkin", the type retrieval would fail
|
||||
// for user modified skins. This aims to amicably handle that.
|
||||
// If we ever add more default skins in the future this will need some kind of proper migration rather than
|
||||
// a single fallback.
|
||||
return new TrianglesSkin(this, resources);
|
||||
}
|
||||
|
||||
return (Skin)Activator.CreateInstance(type, this, resources);
|
||||
}
|
||||
|
@ -49,9 +49,9 @@ namespace osu.Game.Skinning
|
||||
|
||||
public readonly Bindable<Skin> CurrentSkin = new Bindable<Skin>();
|
||||
|
||||
public readonly Bindable<Live<SkinInfo>> CurrentSkinInfo = new Bindable<Live<SkinInfo>>(Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged())
|
||||
public readonly Bindable<Live<SkinInfo>> CurrentSkinInfo = new Bindable<Live<SkinInfo>>(TrianglesSkin.CreateInfo().ToLiveUnmanaged())
|
||||
{
|
||||
Default = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged()
|
||||
Default = TrianglesSkin.CreateInfo().ToLiveUnmanaged()
|
||||
};
|
||||
|
||||
private readonly SkinImporter skinImporter;
|
||||
@ -59,14 +59,14 @@ namespace osu.Game.Skinning
|
||||
private readonly IResourceStore<byte[]> userFiles;
|
||||
|
||||
/// <summary>
|
||||
/// The default skin.
|
||||
/// The default "triangles" skin.
|
||||
/// </summary>
|
||||
public Skin DefaultSkin { get; }
|
||||
public Skin DefaultSkinTriangles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The default legacy skin.
|
||||
/// The default "classic" skin.
|
||||
/// </summary>
|
||||
public Skin DefaultLegacySkin { get; }
|
||||
public Skin DefaultClassicSkin { get; }
|
||||
|
||||
public SkinManager(Storage storage, RealmAccess realm, GameHost host, IResourceStore<byte[]> resources, AudioManager audio, Scheduler scheduler)
|
||||
: base(storage, realm)
|
||||
@ -85,8 +85,8 @@ namespace osu.Game.Skinning
|
||||
|
||||
var defaultSkins = new[]
|
||||
{
|
||||
DefaultLegacySkin = new DefaultLegacySkin(this),
|
||||
DefaultSkin = new DefaultSkin(this),
|
||||
DefaultClassicSkin = new DefaultLegacySkin(this),
|
||||
DefaultSkinTriangles = new TrianglesSkin(this),
|
||||
};
|
||||
|
||||
// Ensure the default entries are present.
|
||||
@ -104,7 +104,7 @@ namespace osu.Game.Skinning
|
||||
CurrentSkin.Value = skin.NewValue.PerformRead(GetSkin);
|
||||
};
|
||||
|
||||
CurrentSkin.Value = DefaultSkin;
|
||||
CurrentSkin.Value = DefaultSkinTriangles;
|
||||
CurrentSkin.ValueChanged += skin =>
|
||||
{
|
||||
if (!skin.NewValue.SkinInfo.Equals(CurrentSkinInfo.Value))
|
||||
@ -119,11 +119,13 @@ namespace osu.Game.Skinning
|
||||
Realm.Run(r =>
|
||||
{
|
||||
// choose from only user skins, removing the current selection to ensure a new one is chosen.
|
||||
var randomChoices = r.All<SkinInfo>().Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID).ToArray();
|
||||
var randomChoices = r.All<SkinInfo>()
|
||||
.Where(s => !s.DeletePending && s.ID != CurrentSkinInfo.Value.ID)
|
||||
.ToArray();
|
||||
|
||||
if (randomChoices.Length == 0)
|
||||
{
|
||||
CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged();
|
||||
CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -227,11 +229,11 @@ namespace osu.Game.Skinning
|
||||
{
|
||||
yield return CurrentSkin.Value;
|
||||
|
||||
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultLegacySkin)
|
||||
yield return DefaultLegacySkin;
|
||||
if (CurrentSkin.Value is LegacySkin && CurrentSkin.Value != DefaultClassicSkin)
|
||||
yield return DefaultClassicSkin;
|
||||
|
||||
if (CurrentSkin.Value != DefaultSkin)
|
||||
yield return DefaultSkin;
|
||||
if (CurrentSkin.Value != DefaultSkinTriangles)
|
||||
yield return DefaultSkinTriangles;
|
||||
}
|
||||
}
|
||||
|
||||
@ -292,10 +294,26 @@ namespace osu.Game.Skinning
|
||||
Guid currentUserSkin = CurrentSkinInfo.Value.ID;
|
||||
|
||||
if (items.Any(s => s.ID == currentUserSkin))
|
||||
scheduler.Add(() => CurrentSkinInfo.Value = Skinning.DefaultSkin.CreateInfo().ToLiveUnmanaged());
|
||||
scheduler.Add(() => CurrentSkinInfo.Value = TrianglesSkin.CreateInfo().ToLiveUnmanaged());
|
||||
|
||||
Delete(items.ToList(), silent);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetSkinFromConfiguration(string guidString)
|
||||
{
|
||||
Live<SkinInfo> skinInfo = null;
|
||||
|
||||
if (Guid.TryParse(guidString, out var guid))
|
||||
skinInfo = Query(s => s.ID == guid);
|
||||
|
||||
if (skinInfo == null)
|
||||
{
|
||||
if (guid == SkinInfo.CLASSIC_SKIN)
|
||||
skinInfo = DefaultClassicSkin.SkinInfo;
|
||||
}
|
||||
|
||||
CurrentSkinInfo.Value = skinInfo ?? DefaultSkinTriangles.SkinInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +112,7 @@ namespace osu.Game.Skinning
|
||||
|
||||
// Temporarily used to exclude undesirable ISkin implementations
|
||||
static bool isUserSkin(ISkin skin)
|
||||
=> skin.GetType() == typeof(DefaultSkin)
|
||||
=> skin.GetType() == typeof(TrianglesSkin)
|
||||
|| skin.GetType() == typeof(DefaultLegacySkin)
|
||||
|| skin.GetType() == typeof(LegacySkin);
|
||||
}
|
||||
|
@ -22,26 +22,26 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
public class DefaultSkin : Skin
|
||||
public class TrianglesSkin : Skin
|
||||
{
|
||||
public static SkinInfo CreateInfo() => new SkinInfo
|
||||
{
|
||||
ID = osu.Game.Skinning.SkinInfo.DEFAULT_SKIN,
|
||||
Name = "osu! (triangles)",
|
||||
ID = osu.Game.Skinning.SkinInfo.TRIANGLES_SKIN,
|
||||
Name = "osu! \"triangles\" (2017)",
|
||||
Creator = "team osu!",
|
||||
Protected = true,
|
||||
InstantiationInfo = typeof(DefaultSkin).GetInvariantInstantiationInfo()
|
||||
InstantiationInfo = typeof(TrianglesSkin).GetInvariantInstantiationInfo()
|
||||
};
|
||||
|
||||
private readonly IStorageResourceProvider resources;
|
||||
|
||||
public DefaultSkin(IStorageResourceProvider resources)
|
||||
public TrianglesSkin(IStorageResourceProvider resources)
|
||||
: this(CreateInfo(), resources)
|
||||
{
|
||||
}
|
||||
|
||||
[UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)]
|
||||
public DefaultSkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
public TrianglesSkin(SkinInfo skin, IStorageResourceProvider resources)
|
||||
: base(skin, resources)
|
||||
{
|
||||
this.resources = resources;
|
@ -7,7 +7,6 @@ using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
@ -57,7 +56,9 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
protected virtual bool Autoplay => false;
|
||||
|
||||
protected void LoadPlayer()
|
||||
protected void LoadPlayer() => LoadPlayer(Array.Empty<Mod>());
|
||||
|
||||
protected void LoadPlayer(Mod[] mods)
|
||||
{
|
||||
var ruleset = CreatePlayerRuleset();
|
||||
Ruleset.Value = ruleset.RulesetInfo;
|
||||
@ -65,20 +66,21 @@ namespace osu.Game.Tests.Visual
|
||||
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
|
||||
|
||||
Beatmap.Value = CreateWorkingBeatmap(beatmap);
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
|
||||
SelectedMods.Value = mods;
|
||||
|
||||
if (!AllowFail)
|
||||
{
|
||||
var noFailMod = ruleset.CreateMod<ModNoFail>();
|
||||
if (noFailMod != null)
|
||||
SelectedMods.Value = new[] { noFailMod };
|
||||
SelectedMods.Value = SelectedMods.Value.Append(noFailMod).ToArray();
|
||||
}
|
||||
|
||||
if (Autoplay)
|
||||
{
|
||||
var mod = ruleset.GetAutoplayMod();
|
||||
if (mod != null)
|
||||
SelectedMods.Value = SelectedMods.Value.Concat(mod.Yield()).ToArray();
|
||||
SelectedMods.Value = SelectedMods.Value.Append(mod).ToArray();
|
||||
}
|
||||
|
||||
Player = CreatePlayer(ruleset);
|
||||
|
@ -29,8 +29,9 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
public abstract class SkinnableTestScene : OsuGridTestScene, IStorageResourceProvider
|
||||
{
|
||||
private TrianglesSkin trianglesSkin;
|
||||
private Skin metricsSkin;
|
||||
private Skin defaultSkin;
|
||||
private Skin legacySkin;
|
||||
private Skin specialSkin;
|
||||
private Skin oldSkin;
|
||||
|
||||
@ -47,8 +48,9 @@ namespace osu.Game.Tests.Visual
|
||||
{
|
||||
var dllStore = new DllResourceStore(GetType().Assembly);
|
||||
|
||||
trianglesSkin = new TrianglesSkin(this);
|
||||
metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/metrics_skin"), this, true);
|
||||
defaultSkin = new DefaultLegacySkin(this);
|
||||
legacySkin = new DefaultLegacySkin(this);
|
||||
specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/special_skin"), this, true);
|
||||
oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore<byte[]>(dllStore, "Resources/old_skin"), this, true);
|
||||
}
|
||||
@ -61,9 +63,9 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
var beatmap = CreateBeatmapForSkinProvider();
|
||||
|
||||
Cell(0).Child = createProvider(null, creationFunction, beatmap);
|
||||
Cell(0).Child = createProvider(trianglesSkin, creationFunction, beatmap);
|
||||
Cell(1).Child = createProvider(metricsSkin, creationFunction, beatmap);
|
||||
Cell(2).Child = createProvider(defaultSkin, creationFunction, beatmap);
|
||||
Cell(2).Child = createProvider(legacySkin, creationFunction, beatmap);
|
||||
Cell(3).Child = createProvider(specialSkin, creationFunction, beatmap);
|
||||
Cell(4).Child = createProvider(oldSkin, creationFunction, beatmap);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Humanizer;
|
||||
using Humanizer.Localisation;
|
||||
|
||||
namespace osu.Game.Utils
|
||||
{
|
||||
@ -26,5 +27,27 @@ namespace osu.Game.Utils
|
||||
return input.Humanize(culture: new CultureInfo("en-US"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Turns the current or provided timespan into a human readable sentence
|
||||
/// </summary>
|
||||
/// <param name="input">The date to be humanized</param>
|
||||
/// <param name="precision">The maximum number of time units to return. Defaulted is 1 which means the largest unit is returned</param>
|
||||
/// <param name="maxUnit">The maximum unit of time to output. The default value is <see cref="TimeUnit.Week"/>. The time units <see cref="TimeUnit.Month"/> and <see cref="TimeUnit.Year"/> will give approximations for time spans bigger 30 days by calculating with 365.2425 days a year and 30.4369 days a month.</param>
|
||||
/// <param name="minUnit">The minimum unit of time to output.</param>
|
||||
/// <param name="toWords">Uses words instead of numbers if true. E.g. one day.</param>
|
||||
/// <returns>distance of time in words</returns>
|
||||
public static string Humanize(TimeSpan input, int precision = 1, TimeUnit maxUnit = TimeUnit.Week, TimeUnit minUnit = TimeUnit.Millisecond, bool toWords = false)
|
||||
{
|
||||
// this works around https://github.com/xamarin/xamarin-android/issues/2012 and https://github.com/Humanizr/Humanizer/issues/690#issuecomment-368536282
|
||||
try
|
||||
{
|
||||
return input.Humanize(precision: precision, maxUnit: maxUnit, minUnit: minUnit);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return input.Humanize(culture: new CultureInfo("en-US"), precision: precision, maxUnit: maxUnit, minUnit: minUnit);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.15.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.908.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.916.1" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
<PackageReference Include="Sentry" Version="3.20.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
|
@ -61,7 +61,7 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.908.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.916.1" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.831.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||
@ -82,7 +82,7 @@
|
||||
<PackageReference Include="DiffPlex" Version="1.7.1" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.908.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.916.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
Loading…
x
Reference in New Issue
Block a user