mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 11:37:28 +08:00
Merge branch 'master' into argon-skin
This commit is contained in:
commit
4492a26b59
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
SetContents(skin =>
|
||||
{
|
||||
var implementation = skin is not TrianglesSkin
|
||||
var implementation = skin is LegacySkin
|
||||
? CreateLegacyImplementation()
|
||||
: CreateDefaultImplementation();
|
||||
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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();
|
||||
|
@ -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")]
|
||||
|
@ -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)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -716,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;
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -46,20 +45,18 @@ 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);
|
||||
|
||||
try
|
||||
if (type == null)
|
||||
{
|
||||
return (Skin)Activator.CreateInstance(type, this, resources);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Since the class was renamed from "DefaultSkin" to "TrianglesSkin", the instantiation would fail
|
||||
// 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 catch.
|
||||
// a single fallback.
|
||||
return new TrianglesSkin(this, resources);
|
||||
}
|
||||
|
||||
return (Skin)Activator.CreateInstance(type, this, resources);
|
||||
}
|
||||
|
||||
public IList<RealmNamedFileUsage> Files { get; } = null!;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user