1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 15:47:26 +08:00

Merge branch 'master' into multiplayer-auto-countdown

This commit is contained in:
Dan Balasescu 2022-03-24 18:11:51 +09:00
commit 40eca0fbe2
74 changed files with 2026 additions and 720 deletions

View File

@ -27,7 +27,7 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.1210.0",
"version": "2022.320.0",
"commands": [
"localisation"
]

72
.github/ISSUE_TEMPLATE/bug-issue.yml vendored Normal file
View File

@ -0,0 +1,72 @@
name: Bug report
description: Report a very clearly broken issue.
body:
- type: markdown
attributes:
value: |
# osu! bug report
Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown
attributes:
label: Type
options:
- Crash to desktop
- Game behaviour
- Performance
- Cosmetic
- Other
validations:
required: true
- type: textarea
attributes:
label: Bug description
description: How did you find the bug? Any additional details that might help?
validations:
required: true
- type: textarea
attributes:
label: Screenshots or videos
description: Add screenshots or videos that show the bug here.
placeholder: Drag and drop the screenshots/videos into this box.
validations:
required: false
- type: input
attributes:
label: Version
description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
validations:
required: true
- type: markdown
attributes:
value: |
## Logs
Attaching log files is required for every reported bug. See instructions below on how to find them.
If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
The default places to find the logs are as follows:
- `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there.
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea
attributes:
label: Logs
placeholder: Drag and drop the log files into this box.
validations:
required: true

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-dotnettools.csharp"
]
}

View File

@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:**
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock;

View File

@ -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.Linq;
using osu.Framework.Bindables;
using osu.Game.Configuration;
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.

View File

@ -0,0 +1,148 @@
// 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.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider slider)
{
slider.Tracking.ValueChanged += e =>
{
if (e.NewValue || slider.Judged) return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
tail.MissForcefully();
};
}
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
var osuBeatmap = (OsuBeatmap)beatmap;
if (osuBeatmap.HitObjects.Count == 0) return;
var hitObjects = osuBeatmap.HitObjects.Select(ho =>
{
if (ho is Slider slider)
{
var newSlider = new StrictTrackingSlider(slider);
return newSlider;
}
return ho;
}).ToList();
osuBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
drawableRuleset.Playfield.RegisterPool<StrictTrackingSliderTailCircle, StrictTrackingDrawableSliderTail>(10, 100);
}
private class StrictTrackingSliderTailCircle : SliderTailCircle
{
public StrictTrackingSliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
}
private class StrictTrackingDrawableSliderTail : DrawableSliderTail
{
public override bool DisplayResult => true;
}
private class StrictTrackingSlider : Slider
{
public StrictTrackingSlider(Slider original)
{
StartTime = original.StartTime;
Samples = original.Samples;
Path = original.Path;
NodeSamples = original.NodeSamples;
RepeatCount = original.RepeatCount;
Position = original.Position;
NewCombo = original.NewCombo;
ComboOffset = original.ComboOffset;
LegacyLastTickOffset = original.LegacyLastTickOffset;
TickDistanceMultiplier = original.TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
foreach (var e in sliderEvents)
{
switch (e.Type)
{
case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
});
break;
case SliderEventType.LegacyLastTick:
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
{
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
});
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
}
}
UpdateNestedSamples();
}
}
}
}

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
}
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
}
}
updateNestedSamples();
UpdateNestedSamples();
}
private void updateNestedPositions()
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition;
}
private void updateNestedSamples()
protected void UpdateNestedSamples()
{
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)

View File

@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
};
case ModType.Conversion:

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component)
{
case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);

View File

@ -590,6 +590,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -597,6 +599,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
});
}
@ -646,6 +649,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -653,6 +658,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
});
}

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Screens.Edit.Setup;
using osu.Game.Storyboards;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
@ -63,13 +65,19 @@ namespace osu.Game.Tests.Visual.Editing
EditorBeatmap editorBeatmap = null;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () =>
AddStep("exit without save", () => Editor.Exit());
AddStep("hold to confirm", () =>
{
Editor.Exit();
DialogOverlay.CurrentDialog.PerformOkAction();
var confirmButton = DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogDangerousButton>().First();
InputManager.MoveMouseTo(confirmButton);
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
}

View File

@ -17,6 +17,13 @@ namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneEditorSaving : EditorSavingTestScene
{
[Test]
public void TestCantExitWithoutSaving()
{
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}
[Test]
public void TestMetadata()
{

View File

@ -1,14 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -29,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reload skin editor", () =>
{
skinEditor?.Expire();
Player.ScaleTo(0.8f);
Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}
@ -40,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
}
[Test]
public void TestEditComponent()
{
BarHitErrorMeter hitErrorMeter = null;
AddStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
}
}

View File

@ -70,6 +70,56 @@ namespace osu.Game.Tests.Visual.Gameplay
});
}
[Test]
public void TestSeekToGameplayStartFramesArriveAfterPlayerLoad()
{
const double gameplay_start = 10000;
loadSpectatingScreen();
start();
waitForPlayer();
sendFrames(startTime: gameplay_start);
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
}
/// <summary>
/// Tests the same as <see cref="TestSeekToGameplayStartFramesArriveAfterPlayerLoad"/> but with the frames arriving just as <see cref="Player"/> is transitioning into existence.
/// </summary>
[Test]
public void TestSeekToGameplayStartFramesArriveAsPlayerLoaded()
{
const double gameplay_start = 10000;
loadSpectatingScreen();
start();
AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true);
AddUntilStep("queue send frames on player load", () =>
{
var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer;
if (loadingPlayer == null)
return false;
loadingPlayer.OnLoadComplete += _ =>
{
spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start);
};
return true;
});
waitForPlayer();
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
}
[Test]
public void TestFrameStarvationAndResume()
{
@ -319,9 +369,9 @@ namespace osu.Game.Tests.Visual.Gameplay
private void checkPaused(bool state) =>
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
private void sendFrames(int count = 10)
private void sendFrames(int count = 10, double startTime = 0)
{
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count));
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
}
private void loadSpectatingScreen()

View File

@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected void RunGameplay()
{
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());

View File

@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any());

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -27,9 +28,9 @@ using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
public class TestSceneMatchStartControl : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private MatchStartControl control;
private BeatmapSetInfo importedSet;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = button = new MultiplayerReadyButton
Child = control = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -74,37 +75,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestStartWithCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
}
[Test]
public void TestCancelCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
}
@ -117,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.TransferHost(2);
});
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }));
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
@ -132,25 +133,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
}
[Test]
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
{
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
@ -158,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
}
@ -168,12 +169,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
@ -181,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.ReadyButton>().Single().Enabled.Value);
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value);
}
[Test]
@ -193,13 +194,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.TransferHost(2);
});
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }));
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
}
@ -211,7 +212,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
readyButton = control.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
@ -230,10 +231,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
}
@ -249,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
verifyGameplayStartFlow();
@ -264,7 +265,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.TransferHost(2);
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
verifyGameplayStartFlow();
@ -279,14 +280,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
});
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
[TestCase(true)]
@ -304,7 +305,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
if (!isHost)
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddRepeatStep("change user ready state", () =>
{
@ -322,7 +323,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void verifyGameplayStartFlow()
{
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
AddStep("finish gameplay", () =>
@ -331,7 +332,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
});
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
}
}
}

View File

@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
using osu.Game.Tests.Resources;
using osuTK.Input;
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -424,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -462,7 +463,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -500,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
@ -535,7 +536,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
}
@ -568,7 +569,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("start match externally", () => multiplayerClient.StartMatch());
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
AddStep("restore beatmap", () =>
{
@ -883,7 +884,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("start match by other user", () =>
{
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
multiplayerClient.StartMatch();
multiplayerClient.StartMatch().WaitSafely();
});
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);

View File

@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
}

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -129,6 +131,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
}
[Test]
public void TestQueueTabCount()
{
assertQueueTabCount(1);
addItemStep();
assertQueueTabCount(2);
addItemStep();
assertQueueTabCount(3);
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
assertQueueTabCount(2);
AddStep("leave room", () => RoomManager.PartRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
assertQueueTabCount(0);
}
[Ignore("Expired items are initially removed from the room.")]
[Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
@ -213,6 +234,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
private void assertQueueTabCount(int count)
{
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
{
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()
.Single(t => t.Value == MultiplayerPlaylistDisplayMode.Queue)
.ChildrenOfType<OsuSpriteText>().Single().Text == queueTabText;
});
}
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
private bool inQueueList(int playlistItemId)

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
{
private MultiplayerSpectateButton spectateButton;
private MultiplayerReadyButton readyButton;
private MatchStartControl startControl;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
},
readyButton = new MultiplayerReadyButton
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
private void assertReadyButtonEnablement(bool shouldBeEnabled)
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
@ -21,10 +23,12 @@ using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO;
using osuTK;
using osuTK.Input;
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestEditComponentDuringGameplay()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
SkinEditor skinEditor = null;
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
AddStep("Click gameplay scene button", () =>
{
skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick();
});
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return Game.ScreenStack.CurrentScreen is Player;
});
BarHitErrorMeter hitErrorMeter = null;
AddUntilStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
if (blueprint == null)
return false;
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
return true;
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test]
public void TestRetryCountIncrements()
{
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
@ -152,7 +224,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
@ -262,6 +335,20 @@ namespace osu.Game.Tests.Visual.Navigation
exitViaBackButtonAndConfirm();
}
[Test]
public void TestModsResetOnEnteringMultiplayer()
{
var osuAutomationMod = new OsuModAutoplay();
AddStep("Enable autoplay", () => { Game.SelectedMods.Value = new[] { osuAutomationMod }; });
PushAndConfirm(() => new Screens.OnlinePlay.Multiplayer.Multiplayer());
AddUntilStep("Mods are removed", () => Game.SelectedMods.Value.Count == 0);
AddStep("Return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
AddUntilStep("Mods are restored", () => Game.SelectedMods.Value.Contains(osuAutomationMod));
}
[Test]
public void TestExitMultiWithEscape()
{

View File

@ -0,0 +1,163 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.ChannelList;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelListItem : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> selected = new Bindable<Channel>();
private static readonly List<Channel> channels = new List<Channel>
{
createPublicChannel("#public-channel"),
createPublicChannel("#public-channel-long-name"),
createPrivateChannel("test user", 2),
createPrivateChannel("test user long name", 3),
};
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
private FillFlowContainer flow;
private OsuSpriteText selectedText;
private OsuSpriteText leaveText;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
foreach (var item in channelMap.Values)
item.Expire();
channelMap.Clear();
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(10),
Children = new Drawable[]
{
selectedText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},
leaveText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 16,
AlwaysPresent = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
Width = 190,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
flow = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
},
},
};
selected.BindValueChanged(change =>
{
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
}, true);
foreach (var channel in channels)
{
var item = new ChannelListItem(channel);
flow.Add(item);
channelMap.Add(channel, item);
item.OnRequestSelect += c => selected.Value = c;
item.OnRequestLeave += leaveChannel;
}
});
}
[Test]
public void TestVisual()
{
AddStep("Select second item", () => selected.Value = channels.Skip(1).First());
AddStep("Unread Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = true;
});
AddStep("Read Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Unread.Value = false;
});
AddStep("Add Mention Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value++;
});
AddStep("Add 98 Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value += 98;
});
AddStep("Clear Mentions Selected", () =>
{
if (selected.Value != null)
channelMap[selected.Value].Mentions.Value = 0;
});
}
private void leaveChannel(Channel channel)
{
leaveText.Text = $"OnRequestLeave: {channel.Name}";
leaveText.FadeOutFromOne(1000, Easing.InQuint);
}
private static Channel createPublicChannel(string name) =>
new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
private static Channel createPrivateChannel(string username, int id)
=> new Channel(new APIUser { Id = id, Username = username });
}
}

View File

@ -40,6 +40,10 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Text = @"You're a fake!",
},
new PopupDialogDangerousButton
{
Text = @"Careful with this one..",
},
};
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms;
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
[Ignored]
public APIBeatmap? OnlineInfo { get; set; }
/// <summary>
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
/// Todo: This should never be used nor exist, but is still relied on in <see cref="ScoresContainer.Scores"/> since <see cref="IBeatmapInfo"/> can't be used yet. For now this is obsoleted until it is removed.
/// </summary>
[Ignored]
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
public int? MaxCombo { get; set; }
[Ignored]

View File

@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
[NotMapped]
public APIBeatmap OnlineInfo { get; set; }
[NotMapped]
public int? MaxCombo { get; set; }
/// <summary>
/// The playable length in milliseconds of this beatmap.
/// </summary>

View File

@ -295,7 +295,6 @@ namespace osu.Game.Database
TimelineZoom = beatmap.TimelineZoom,
Countdown = beatmap.Countdown,
CountdownOffset = beatmap.CountdownOffset,
MaxCombo = beatmap.MaxCombo,
Bookmarks = beatmap.Bookmarks,
BeatmapSet = realmBeatmapSet,
};

View File

@ -28,6 +28,14 @@ namespace osu.Game.Graphics.Containers
/// </summary>
protected virtual bool AllowMultipleFires => false;
/// <summary>
/// Specify a custom activation delay, overriding the game-wide user setting.
/// </summary>
/// <remarks>
/// This should be used in special cases where we want to be extra sure the user knows what they are doing. An example is when changes would be lost.
/// </remarks>
protected virtual double? HoldActivationDelay => null;
public Bindable<double> Progress = new BindableDouble();
private Bindable<double> holdActivationDelay;
@ -35,7 +43,9 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
holdActivationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
holdActivationDelay = HoldActivationDelay != null
? new Bindable<double>(HoldActivationDelay.Value)
: config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
}
protected void BeginConfirm()

View File

@ -45,8 +45,9 @@ namespace osu.Game.Graphics.UserInterface
}
}
protected readonly Container ColourContainer;
private readonly Container backgroundContainer;
private readonly Container colourContainer;
private readonly Container glowContainer;
private readonly Box leftGlow;
private readonly Box centerGlow;
@ -113,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
Masking = true,
Children = new Drawable[]
{
colourContainer = new Container
ColourContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
@ -182,7 +183,7 @@ namespace osu.Game.Graphics.UserInterface
{
buttonColour = value;
updateGlow();
colourContainer.Colour = value;
ColourContainer.Colour = value;
}
}
@ -230,11 +231,11 @@ namespace osu.Game.Graphics.UserInterface
Alpha = 0.05f
};
colourContainer.Add(flash);
ColourContainer.Add(flash);
flash.FadeOutFromOne(100).Expire();
clickAnimating = true;
colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint)
ColourContainer.ResizeWidthTo(ColourContainer.Width * 1.05f, 100, Easing.OutQuint)
.OnComplete(_ =>
{
clickAnimating = false;
@ -246,14 +247,14 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnMouseDown(MouseDownEvent e)
{
colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
ColourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (State == SelectionState.Selected)
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
ColourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
base.OnMouseUp(e);
}
@ -279,12 +280,12 @@ namespace osu.Game.Graphics.UserInterface
if (newState == SelectionState.Selected)
{
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
glowContainer.FadeIn(hover_duration, Easing.OutQuint);
}
else
{
colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
glowContainer.FadeOut(hover_duration, Easing.OutQuint);
}

View File

@ -18,6 +18,6 @@ namespace osu.Game.Online.Multiplayer.Countdown
/// How long the countdown should last.
/// </summary>
[Key(0)]
public TimeSpan Delay { get; set; }
public TimeSpan Duration { get; set; }
}
}

View File

@ -5,6 +5,7 @@
using System;
using MessagePack;
using osu.Game.Online.Multiplayer.Countdown;
namespace osu.Game.Online.Multiplayer
{
@ -16,9 +17,12 @@ namespace osu.Game.Online.Multiplayer
public abstract class MultiplayerCountdown
{
/// <summary>
/// The time at which the countdown will end.
/// The amount of time remaining in the countdown.
/// </summary>
/// <remarks>
/// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownChangedEvent"/>.
/// </remarks>
[Key(0)]
public DateTimeOffset EndTime { get; set; }
public TimeSpan TimeRemaining { get; set; }
}
}

View File

@ -1046,6 +1046,10 @@ namespace osu.Game
switch (e.Action)
{
case GlobalAction.ToggleSkinEditor:
skinEditor.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings:
Host.ResetInputHandlers();
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();

View File

@ -173,7 +173,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
Text = score.MaxCombo.ToLocalisableString(@"0\x"),
Font = OsuFont.GetFont(size: text_size),
#pragma warning disable 618
Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
#pragma warning restore 618
}
};

View File

@ -78,7 +78,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
// TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`.
var beatmapInfo = new BeatmapInfo
{
#pragma warning disable 618
MaxCombo = apiBeatmap.MaxCombo,
#pragma warning restore 618
Status = apiBeatmap.Status,
MD5Hash = apiBeatmap.MD5Hash
};

View File

@ -0,0 +1,171 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Chat;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelListItem : OsuClickableContainer
{
public event Action<Channel>? OnRequestSelect;
public event Action<Channel>? OnRequestLeave;
public readonly BindableInt Mentions = new BindableInt();
public readonly BindableBool Unread = new BindableBool();
private readonly Channel channel;
private Box? hoverBox;
private Box? selectBox;
private OsuSpriteText? text;
private ChannelListItemCloseButton? close;
[Resolved]
private Bindable<Channel> selectedChannel { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ChannelListItem(Channel channel)
{
this.channel = channel;
}
[BackgroundDependencyLoader]
private void load()
{
Height = 30;
RelativeSizeAxes = Axes.X;
Children = new Drawable[]
{
hoverBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
Alpha = 0f,
},
selectBox = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
Alpha = 0f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{
createIcon(),
text = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
Truncate = true,
},
new ChannelListItemMentionPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 3 },
Mentions = { BindTarget = Mentions },
},
close = new ChannelListItemCloseButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Right = 3 },
Action = () => OnRequestLeave?.Invoke(channel),
}
}
},
},
},
};
Action = () => OnRequestSelect?.Invoke(channel);
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedChannel.BindValueChanged(change =>
{
if (change.NewValue == channel)
selectBox?.FadeIn(300, Easing.OutQuint);
else
selectBox?.FadeOut(200, Easing.OutQuint);
}, true);
Unread.BindValueChanged(change =>
{
text!.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
}, true);
}
protected override bool OnHover(HoverEvent e)
{
hoverBox?.FadeIn(300, Easing.OutQuint);
close?.FadeIn(300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
hoverBox?.FadeOut(200, Easing.OutQuint);
close?.FadeOut(200, Easing.OutQuint);
base.OnHoverLost(e);
}
private Drawable createIcon()
{
if (channel.Type != ChannelType.PM)
return Drawable.Empty();
return new UpdateableAvatar(channel.Users.First(), isInteractive: false)
{
Size = new Vector2(20),
Margin = new MarginPadding { Right = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
CornerRadius = 10,
Masking = true,
};
}
}
}

View File

@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelListItemCloseButton : OsuClickableContainer
{
private SpriteIcon icon = null!;
private Color4 normalColour;
private Color4 hoveredColour;
[BackgroundDependencyLoader]
private void load(OsuColour osuColour)
{
normalColour = osuColour.Red2;
hoveredColour = Color4.White;
Alpha = 0f;
Size = new Vector2(20);
Add(icon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.75f),
Icon = FontAwesome.Solid.TimesCircle,
RelativeSizeAxes = Axes.Both,
Colour = normalColour,
});
}
// Transforms matching OsuAnimatedButton
protected override bool OnHover(HoverEvent e)
{
icon.FadeColour(hoveredColour, 300, Easing.OutQuint);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
icon.FadeColour(normalColour, 300, Easing.OutQuint);
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
icon.ScaleTo(0.75f, 2000, Easing.OutQuint);
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
icon.ScaleTo(1, 1000, Easing.OutElastic);
base.OnMouseUp(e);
}
}
}

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Chat.ChannelList
{
public class ChannelListItemMentionPill : CircularContainer
{
public readonly BindableInt Mentions = new BindableInt();
private OsuSpriteText countText = null!;
private Box box = null!;
[BackgroundDependencyLoader]
private void load(OsuColour osuColour, OverlayColourProvider colourProvider)
{
Masking = true;
Size = new Vector2(20, 12);
Alpha = 0f;
Children = new Drawable[]
{
box = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = osuColour.Orange1,
},
countText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Torus.With(size: 11, weight: FontWeight.Bold),
Margin = new MarginPadding { Bottom = 1 },
Colour = colourProvider.Background5,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Mentions.BindValueChanged(change =>
{
int mentionCount = change.NewValue;
countText.Text = mentionCount > 99 ? "99+" : mentionCount.ToString();
if (mentionCount > 0)
{
this.FadeIn(1000, Easing.OutQuint);
box.FlashColour(Color4.White, 500, Easing.OutQuint);
}
else
this.FadeOut(100, Easing.OutQuint);
}, true);
}
}
}

View File

@ -219,7 +219,12 @@ namespace osu.Game.Overlays.Dialog
/// <summary>
/// Programmatically clicks the first <see cref="PopupDialogOkButton"/>.
/// </summary>
public void PerformOkAction() => Buttons.OfType<PopupDialogOkButton>().First().TriggerClick();
public void PerformOkAction() => PerformAction<PopupDialogOkButton>();
/// <summary>
/// Programmatically clicks the first button of the provided type.
/// </summary>
public void PerformAction<T>() where T : PopupDialogButton => Buttons.OfType<T>().First().TriggerClick();
protected override bool OnKeyDown(KeyDownEvent e)
{

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Dialog
{
public class PopupDialogDangerousButton : PopupDialogButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
ButtonColour = colours.Red3;
ColourContainer.Add(new ConfirmFillBox
{
Action = () => Action(),
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
});
}
private class ConfirmFillBox : HoldToConfirmContainer
{
private Box box;
protected override double? HoldActivationDelay => 500;
protected override void LoadComplete()
{
base.LoadComplete();
Child = box = new Box
{
RelativeSizeAxes = Axes.Both,
};
Progress.BindValueChanged(progress => box.Width = (float)progress.NewValue, true);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
BeginConfirm();
return true;
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (!e.HasAnyButtonPressed)
AbortConfirm();
}
}
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsButton
{
Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(),
Action = () => skinEditor?.ToggleVisibility(),
},
new ExportSkinButton(),
};

View File

@ -500,12 +500,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other)
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank;
// The additions to equality checks here are *required* to ensure that pooling works correctly.
// Of note, `IsLayered` may cause the usage of `SampleVirtual` instead of an actual sample (in cases playback is not required).
// Removing it would cause samples which may actually require playback to potentially source for a `SampleVirtual` sample pool.
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered;
public override bool Equals(object? obj)
=> obj is LegacyHitSampleInfo other && Equals(other);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank);
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
}
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>

View File

@ -122,7 +122,19 @@ namespace osu.Game.Rulesets.Scoring
public static class HitResultExtensions
{
/// <summary>
/// Whether a <see cref="HitResult"/> increases/decreases the combo, and affects the combo portion of the score.
/// Whether a <see cref="HitResult"/> increases the combo.
/// </summary>
public static bool IncreasesCombo(this HitResult result)
=> AffectsCombo(result) && IsHit(result);
/// <summary>
/// Whether a <see cref="HitResult"/> breaks the combo and resets it back to zero.
/// </summary>
public static bool BreaksCombo(this HitResult result)
=> AffectsCombo(result) && !IsHit(result);
/// <summary>
/// Whether a <see cref="HitResult"/> increases/breaks the combo, and affects the combo portion of the score.
/// </summary>
public static bool AffectsCombo(this HitResult result)
{

View File

@ -166,20 +166,10 @@ namespace osu.Game.Rulesets.Scoring
if (!result.Type.IsScorable())
return;
if (result.Type.AffectsCombo())
{
switch (result.Type)
{
case HitResult.Miss:
case HitResult.LargeTickMiss:
Combo.Value = 0;
break;
default:
Combo.Value++;
break;
}
}
if (result.Type.IncreasesCombo())
Combo.Value++;
else if (result.Type.BreaksCombo())
Combo.Value = 0;
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;

View File

@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI
/// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
public void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject
where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));

View File

@ -157,7 +157,7 @@ namespace osu.Game.Scoring
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
/// <summary>
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[Ignored]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();

View File

@ -134,35 +134,9 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
return score.TotalScore;
int beatmapMaxCombo;
if (score.IsLegacyScore)
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.BeatmapInfo.MaxCombo != null)
beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value;
else
{
if (difficulties == null)
return score.TotalScore;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
// Something failed during difficulty calculation. Fall back to provided score.
if (difficulty == null)
return score.TotalScore;
beatmapMaxCombo = difficulty.Value.MaxCombo;
}
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false);
if (beatmapMaxCombo == null)
return score.TotalScore;
if (beatmapMaxCombo == 0)
return 0;
@ -171,7 +145,37 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods;
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo));
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value));
}
/// <summary>
/// Retrieves the maximum achievable combo for the provided score.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The maximum achievable combo. A <see langword="null"/> return value indicates the difficulty cache has failed to retrieve the combo.</returns>
public async Task<int?> GetMaximumAchievableComboAsync([NotNull] ScoreInfo score, CancellationToken cancellationToken = default)
{
if (score.IsLegacyScore)
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
#pragma warning disable CS0618
if (score.BeatmapInfo.MaxCombo != null)
return score.BeatmapInfo.MaxCombo.Value;
#pragma warning restore CS0618
if (difficulties == null)
return null;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
return difficulty?.MaxCombo;
}
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
return Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
/// <summary>

View File

@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit
private bool canSave;
private bool exitConfirmed;
protected bool ExitConfirmed { get; private set; }
private string lastSavedHash;
@ -586,7 +586,7 @@ namespace osu.Game.Screens.Edit
public override bool OnExiting(IScreen next)
{
if (!exitConfirmed)
if (!ExitConfirmed)
{
// dialog overlay may not be available in visual tests.
if (dialogOverlay == null)
@ -595,12 +595,9 @@ namespace osu.Game.Screens.Edit
return true;
}
// if the dialog is already displayed, confirm exit with no save.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.PerformOkAction();
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
if (dialogOverlay.CurrentDialog is PromptForSaveDialog)
return true;
}
if (isNewBeatmap || HasUnsavedChanges)
{
@ -645,7 +642,7 @@ namespace osu.Game.Screens.Edit
{
Save();
exitConfirmed = true;
ExitConfirmed = true;
this.Exit();
}
@ -668,7 +665,7 @@ namespace osu.Game.Screens.Edit
Beatmap.SetDefault();
}
exitConfirmed = true;
ExitConfirmed = true;
this.Exit();
}

View File

@ -17,12 +17,12 @@ namespace osu.Game.Screens.Edit
Buttons = new PopupDialogButton[]
{
new PopupDialogCancelButton
new PopupDialogOkButton
{
Text = @"Save my masterpiece!",
Action = saveAndExit
},
new PopupDialogOkButton
new PopupDialogDangerousButton
{
Text = @"Forget all changes",
Action = exit

View File

@ -14,18 +14,20 @@ namespace osu.Game.Screens.OnlinePlay.Components
public abstract class ReadyButton : TriangleButton, IHasTooltip
{
public new readonly BindableBool Enabled = new BindableBool();
protected readonly IBindable<BeatmapAvailability> Availability = new Bindable<BeatmapAvailability>();
private readonly IBindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
[BackgroundDependencyLoader]
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
{
Availability.BindTo(beatmapTracker.Availability);
Availability.BindValueChanged(_ => updateState());
availability.BindTo(beatmapTracker.Availability);
availability.BindValueChanged(_ => updateState());
Enabled.BindValueChanged(_ => updateState(), true);
}
private void updateState() =>
base.Enabled.Value = Availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
public virtual LocalisableString TooltipText
{
@ -34,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
if (Enabled.Value)
return string.Empty;
if (Availability.Value.State != DownloadState.LocallyAvailable)
if (availability.Value.State != DownloadState.LocallyAvailable)
return "Beatmap not downloaded";
return string.Empty;

View File

@ -0,0 +1,224 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MatchStartControl : MultiplayerRoomComposite
{
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[CanBeNull]
private IDisposable clickOperation;
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
private readonly BindableBool enabled = new BindableBool();
private readonly MultiplayerCountdownButton countdownButton;
private int countReady;
private ScheduledDelegate readySampleDelegate;
private IBindable<bool> operationInProgress;
public MatchStartControl()
{
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new MultiplayerReadyButton
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = onReadyClick,
Enabled = { BindTarget = enabled },
},
countdownButton = new MultiplayerCountdownButton
{
RelativeSizeAxes = Axes.Y,
Size = new Vector2(40, 1),
Alpha = 0,
Action = startCountdown,
Enabled = { BindTarget = enabled }
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => updateState());
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
updateState();
}
protected override void OnRoomLoadRequested()
{
base.OnRoomLoadRequested();
endOperation();
}
private void onReadyClick()
{
if (Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero)
{
toggleReady();
return;
}
// Local user is the room host and is in a ready state.
// The only action they can take is to stop a countdown if one's currently running.
if (Room.Countdown != null)
{
stopCountdown();
return;
}
// And if a countdown isn't running, start the match.
startMatch();
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
{
// gameplay was not started due to an exception; unblock button.
endOperation();
}
// gameplay is starting, the button will be unblocked on load requested.
});
}
private void startCountdown(TimeSpan duration)
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
}
private void endOperation()
{
clickOperation?.Dispose();
clickOperation = null;
}
private void updateState()
{
if (Room == null)
{
enabled.Value = false;
return;
}
var localUser = Client.LocalUser;
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero)
countdownButton.Alpha = 0;
else
{
switch (localUser?.State)
{
default:
countdownButton.Alpha = 0;
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
break;
}
}
enabled.Value =
Room.State == MultiplayerRoomState.Open
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
if (newCountReady == countReady)
return;
readySampleDelegate?.Cancel();
readySampleDelegate = Schedule(() =>
{
if (newCountReady > countReady)
{
if (newCountReady == newCountTotal)
sampleReadyAll?.Play();
else
sampleReady?.Play();
}
else if (newCountReady < countReady)
{
sampleUnready?.Play();
}
countReady = newCountReady;
});
}
}
}

View File

@ -0,0 +1,83 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerCountdownButton : IconButton, IHasPopover
{
private static readonly TimeSpan[] available_delays =
{
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2)
};
public new Action<TimeSpan> Action;
private readonly Drawable background;
public MultiplayerCountdownButton()
{
Icon = FontAwesome.Solid.CaretDown;
IconScale = new Vector2(0.6f);
Add(background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
});
base.Action = this.ShowPopover;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Green;
}
public Popover GetPopover()
{
var flow = new FillFlowContainer
{
Width = 200,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
};
foreach (var duration in available_delays)
{
flow.Add(new OsuButton
{
RelativeSizeAxes = Axes.X,
Text = $"Start match in {duration.Humanize()}",
BackgroundColour = background.Colour,
Action = () =>
{
Action(duration);
this.HidePopover();
}
});
}
return new OsuPopover { Child = flow };
}
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
RelativeSizeAxes = Axes.Both,
},
null,
new MultiplayerReadyButton
new MatchStartControl
{
RelativeSizeAxes = Axes.Both,
},

View File

@ -2,465 +2,176 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using Humanizer;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.Countdown;
using osuTK;
using osu.Game.Screens.OnlinePlay.Components;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
public class MultiplayerReadyButton : MultiplayerRoomComposite
public class MultiplayerReadyButton : ReadyButton
{
public new Triangles Triangles => base.Triangles;
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[CanBeNull]
private IDisposable clickOperation;
private Sample sampleReady;
private Sample sampleReadyAll;
private Sample sampleUnready;
private readonly BindableBool enabled = new BindableBool();
private readonly CountdownButton countdownButton;
private int countReady;
private ScheduledDelegate readySampleDelegate;
private IBindable<bool> operationInProgress;
public MultiplayerReadyButton()
{
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
new ReadyButton
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = onReadyClick,
Enabled = { BindTarget = enabled },
},
countdownButton = new CountdownButton
{
RelativeSizeAxes = Axes.Y,
Size = new Vector2(40, 1),
Alpha = 0,
Action = startCountdown,
Enabled = { BindTarget = enabled }
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
operationInProgress.BindValueChanged(_ => updateState());
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
}
private MultiplayerRoom room => multiplayerClient.Room;
protected override void LoadComplete()
{
base.LoadComplete();
CurrentPlaylistItem.BindValueChanged(_ => updateState());
multiplayerClient.RoomUpdated += onRoomUpdated;
onRoomUpdated();
}
protected override void OnRoomUpdated()
private MultiplayerCountdown countdown;
private DateTimeOffset countdownReceivedTime;
private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() =>
{
base.OnRoomUpdated();
updateState();
}
if (countdown == null && room?.Countdown != null)
countdownReceivedTime = DateTimeOffset.Now;
protected override void OnRoomLoadRequested()
{
base.OnRoomLoadRequested();
endOperation();
}
countdown = room?.Countdown;
private void onReadyClick()
{
if (Room == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero)
if (room?.Countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
else
{
toggleReady();
countdownUpdateDelegate?.Cancel();
countdownUpdateDelegate = null;
}
updateButtonText();
updateButtonColour();
});
private void updateButtonText()
{
if (room == null)
{
Text = "Ready";
return;
}
// Local user is the room host and is in a ready state.
// The only action they can take is to stop a countdown if one's currently running.
if (Room.Countdown != null)
var localUser = multiplayerClient.LocalUser;
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countText = $"({countReady} / {countTotal} ready)";
if (countdown != null)
{
stopCountdown();
return;
}
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
TimeSpan countdownRemaining;
// And if a countdown isn't running, start the match.
startMatch();
if (timeElapsed > countdown.TimeRemaining)
countdownRemaining = TimeSpan.Zero;
else
countdownRemaining = countdown.TimeRemaining - timeElapsed;
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
void startMatch() => Client.StartMatch().ContinueWith(t =>
{
// accessing Exception here silences any potential errors from the antecedent task
if (t.Exception != null)
switch (localUser?.State)
{
// gameplay was not started due to an exception; unblock button.
endOperation();
default:
Text = $"Ready ({countdownText.ToLowerInvariant()})";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = $"{countdownText} {countText}";
break;
}
// gameplay is starting, the button will be unblocked on load requested.
});
}
private void startCountdown(TimeSpan duration)
{
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation());
}
private void endOperation()
{
clickOperation?.Dispose();
clickOperation = null;
}
private void updateState()
{
if (Room == null)
{
enabled.Value = false;
return;
}
var localUser = Client.LocalUser;
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero)
countdownButton.Alpha = 0;
else
{
switch (localUser?.State)
{
default:
countdownButton.Alpha = 0;
Text = "Ready";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
Text = room.Host?.Equals(localUser) == true
? $"Start match {countText}"
: $"Waiting for host... {countText}";
break;
}
}
}
enabled.Value =
Room.State == MultiplayerRoomState.Open
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
if (newCountReady == countReady)
private void updateButtonColour()
{
if (room == null)
{
setGreen();
return;
readySampleDelegate?.Cancel();
readySampleDelegate = Schedule(() =>
{
if (newCountReady > countReady)
{
if (newCountReady == newCountTotal)
sampleReadyAll?.Play();
else
sampleReady?.Play();
}
else if (newCountReady < countReady)
{
sampleUnready?.Play();
}
countReady = newCountReady;
});
}
public class ReadyButton : Components.ReadyButton
{
public new Triangles Triangles => base.Triangles;
[Resolved]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private OsuColour colours { get; set; }
[CanBeNull]
private MultiplayerRoom room => multiplayerClient.Room;
protected override void LoadComplete()
{
base.LoadComplete();
multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated);
onRoomUpdated();
}
protected override void Update()
var localUser = multiplayerClient.LocalUser;
switch (localUser?.State)
{
base.Update();
if (room?.Countdown != null)
{
// Update the countdown timer.
onRoomUpdated();
}
}
private void onRoomUpdated()
{
updateButtonText();
updateButtonColour();
}
private void updateButtonText()
{
if (room == null)
{
Text = "Ready";
return;
}
var localUser = multiplayerClient.LocalUser;
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countdownText = room.Countdown == null ? string.Empty : $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}";
string countText = $"({countReady} / {countTotal} ready)";
if (room.Countdown != null)
{
switch (localUser?.State)
{
default:
Text = $"Ready ({countdownText.ToLowerInvariant()})";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = $"{countdownText} {countText}";
break;
}
}
else
{
switch (localUser?.State)
{
default:
Text = "Ready";
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
Text = room.Host?.Equals(localUser) == true
? $"Start match {countText}"
: $"Waiting for host... {countText}";
break;
}
}
}
private void updateButtonColour()
{
if (room == null)
{
default:
setGreen();
return;
}
break;
var localUser = multiplayerClient.LocalUser;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
setGreen();
else
setYellow();
if (room.Countdown != null)
{
switch (localUser?.State)
{
default:
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
setYellow();
break;
}
}
else
{
switch (localUser?.State)
{
default:
setGreen();
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (room?.Host?.Equals(localUser) == true)
setGreen();
else
setYellow();
break;
}
}
void setYellow()
{
BackgroundColour = colours.YellowDark;
Triangles.ColourDark = colours.YellowDark;
Triangles.ColourLight = colours.Yellow;
}
void setGreen()
{
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
break;
}
protected override void Dispose(bool isDisposing)
void setYellow()
{
base.Dispose(isDisposing);
if (multiplayerClient != null)
multiplayerClient.RoomUpdated -= onRoomUpdated;
BackgroundColour = colours.YellowDark;
Triangles.ColourDark = colours.YellowDark;
Triangles.ColourLight = colours.Yellow;
}
public override LocalisableString TooltipText
void setGreen()
{
get
{
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
return "Cancel countdown";
return base.TooltipText;
}
BackgroundColour = colours.Green;
Triangles.ColourDark = colours.Green;
Triangles.ColourLight = colours.GreenLight;
}
}
public class CountdownButton : IconButton, IHasPopover
protected override void Dispose(bool isDisposing)
{
private static readonly TimeSpan[] available_delays =
base.Dispose(isDisposing);
if (multiplayerClient != null)
multiplayerClient.RoomUpdated -= onRoomUpdated;
}
public override LocalisableString TooltipText
{
get
{
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2)
};
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
return "Cancel countdown";
public new Action<TimeSpan> Action;
private readonly Drawable background;
public CountdownButton()
{
Icon = FontAwesome.Solid.CaretDown;
IconScale = new Vector2(0.6f);
Add(background = new Box
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
});
base.Action = this.ShowPopover;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
background.Colour = colours.Green;
}
public Popover GetPopover()
{
var flow = new FillFlowContainer
{
Width = 200,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2),
};
foreach (var duration in available_delays)
{
flow.Add(new PopoverButton
{
RelativeSizeAxes = Axes.X,
Text = $"Start match in {duration.Humanize()}",
BackgroundColour = background.Colour,
Action = () =>
{
Action(duration);
this.HidePopover();
}
});
}
return new OsuPopover { Child = flow };
}
public class PopoverButton : OsuButton
{
return base.TooltipText;
}
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
@ -25,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
/// </summary>
public Action<PlaylistItem> RequestEdit;
private MultiplayerPlaylistTabControl playlistTabControl;
private MultiplayerQueueList queueList;
private MultiplayerHistoryList historyList;
private bool firstPopulation = true;
@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
InternalChildren = new Drawable[]
{
new OsuTabControl<MultiplayerPlaylistDisplayMode>
playlistTabControl = new MultiplayerPlaylistTabControl
{
RelativeSizeAxes = Axes.X,
Height = tab_control_height,
@ -64,6 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
}
}
};
playlistTabControl.QueueItems.BindTarget = queueList.Items;
}
protected override void LoadComplete()

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Rooms;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{
public class MultiplayerPlaylistTabControl : OsuTabControl<MultiplayerPlaylistDisplayMode>
{
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
protected override TabItem<MultiplayerPlaylistDisplayMode> CreateTabItem(MultiplayerPlaylistDisplayMode value)
{
if (value == MultiplayerPlaylistDisplayMode.Queue)
return new QueueTabItem { QueueItems = { BindTarget = QueueItems } };
return base.CreateTabItem(value);
}
private class QueueTabItem : OsuTabItem
{
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
public QueueTabItem()
: base(MultiplayerPlaylistDisplayMode.Queue)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
QueueItems.BindCollectionChanged((_, __) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
}
}
}
}

View File

@ -115,6 +115,8 @@ namespace osu.Game.Screens.OnlinePlay
this.FadeIn();
waves.Show();
Mods.SetDefault();
if (loungeSubScreen.IsCurrentScreen())
loungeSubScreen.OnEntering(last);
else

View File

@ -4,12 +4,16 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osuTK;
@ -19,11 +23,27 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
public class BarHitErrorMeter : HitErrorMeter
{
private const int judgement_line_width = 14;
private const int judgement_line_height = 4;
[SettingSource("Judgement line thickness", "How thick the individual lines should be.")]
public BindableNumber<float> JudgementLineThickness { get; } = new BindableNumber<float>(4)
{
MinValue = 1,
MaxValue = 8,
Precision = 0.1f,
};
[SettingSource("Show moving average arrow", "Whether an arrow should move beneath the bar showing the average error.")]
public Bindable<bool> ShowMovingAverage { get; } = new BindableBool(true);
[SettingSource("Centre marker style", "How to signify the centre of the display")]
public Bindable<CentreMarkerStyles> CentreMarkerStyle { get; } = new Bindable<CentreMarkerStyles>(CentreMarkerStyles.Circle);
[SettingSource("Label style", "How to show early/late extremities")]
public Bindable<LabelStyles> LabelStyle { get; } = new Bindable<LabelStyles>(LabelStyles.Icons);
private SpriteIcon arrow;
private SpriteIcon iconEarly;
private SpriteIcon iconLate;
private Drawable labelEarly;
private Drawable labelLate;
private Container colourBarsEarly;
private Container colourBarsLate;
@ -32,6 +52,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private double maxHitWindow;
private double floatingAverage;
private Container colourBars;
private Container arrowContainer;
private (HitResult result, double length)[] hitWindows;
private const int max_concurrent_judgements = 50;
private Drawable[] centreMarkerDrawables;
private const int centre_marker_size = 8;
public BarHitErrorMeter()
{
AutoSizeAxes = Axes.Both;
@ -40,13 +72,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
[BackgroundDependencyLoader]
private void load()
{
const int centre_marker_size = 8;
const int bar_height = 200;
const int bar_width = 2;
const float chevron_size = 8;
const float icon_size = 14;
var hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
InternalChild = new Container
{
@ -65,22 +95,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
iconEarly = new SpriteIcon
{
Y = -10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
},
iconLate = new SpriteIcon
{
Y = 10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
},
colourBarsEarly = new Container
{
Anchor = Anchor.Centre,
@ -98,14 +112,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
},
new Circle
{
Name = "middle marker behind",
Colour = GetColourForHitResult(hitWindows.Last().result),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(centre_marker_size),
},
judgementsContainer = new Container
{
Name = "judgements",
@ -114,24 +120,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Y,
Width = judgement_line_width,
},
new Circle
{
Name = "middle marker in front",
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(centre_marker_size),
Scale = new Vector2(0.5f),
},
}
},
new Container
arrowContainer = new Container
{
Name = "average chevron",
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Origin = Anchor.CentreRight,
Width = chevron_size,
X = chevron_size,
RelativeSizeAxes = Axes.Y,
Alpha = 0,
Scale = new Vector2(0, 1),
Child = arrow = new SpriteIcon
{
Anchor = Anchor.TopCentre,
@ -155,8 +155,180 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
colourBars.Height = 0;
colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
arrow.Alpha = 0;
arrow.Delay(200).FadeInFromZero(600);
CentreMarkerStyle.BindValueChanged(style => recreateCentreMarker(style.NewValue), true);
LabelStyle.BindValueChanged(style => recreateLabels(style.NewValue), true);
// delay the appearance animations for only the initial appearance.
using (arrowContainer.BeginDelayedSequence(450))
{
ShowMovingAverage.BindValueChanged(visible =>
{
arrowContainer.FadeTo(visible.NewValue ? 1 : 0, 250, Easing.OutQuint);
arrowContainer.ScaleTo(visible.NewValue ? new Vector2(1) : new Vector2(0, 1), 250, Easing.OutQuint);
}, true);
}
}
private void recreateCentreMarker(CentreMarkerStyles style)
{
if (centreMarkerDrawables != null)
{
foreach (var d in centreMarkerDrawables)
{
d.ScaleTo(0, 500, Easing.OutQuint)
.FadeOut(500, Easing.OutQuint);
d.Expire();
}
centreMarkerDrawables = null;
}
switch (style)
{
case CentreMarkerStyles.None:
break;
case CentreMarkerStyles.Circle:
centreMarkerDrawables = new Drawable[]
{
new Circle
{
Name = "middle marker behind",
Colour = GetColourForHitResult(hitWindows.Last().result),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Depth = float.MaxValue,
Size = new Vector2(centre_marker_size),
},
new Circle
{
Name = "middle marker in front",
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Depth = float.MinValue,
Size = new Vector2(centre_marker_size / 2f),
},
};
break;
case CentreMarkerStyles.Line:
const float border_size = 1.5f;
centreMarkerDrawables = new Drawable[]
{
new Box
{
Name = "middle marker behind",
Colour = GetColourForHitResult(hitWindows.Last().result),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Depth = float.MaxValue,
Size = new Vector2(judgement_line_width, centre_marker_size / 3f),
},
new Box
{
Name = "middle marker in front",
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Depth = float.MinValue,
Size = new Vector2(judgement_line_width - border_size, centre_marker_size / 3f - border_size),
},
};
break;
default:
throw new ArgumentOutOfRangeException(nameof(style), style, null);
}
if (centreMarkerDrawables != null)
{
foreach (var d in centreMarkerDrawables)
{
colourBars.Add(d);
d.FadeInFromZero(500, Easing.OutQuint)
.ScaleTo(0).ScaleTo(1, 1000, Easing.OutElasticHalf);
}
}
}
private void recreateLabels(LabelStyles style)
{
const float icon_size = 14;
labelEarly?.Expire();
labelEarly = null;
labelLate?.Expire();
labelLate = null;
switch (style)
{
case LabelStyles.None:
break;
case LabelStyles.Icons:
labelEarly = new SpriteIcon
{
Y = -10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
};
labelLate = new SpriteIcon
{
Y = 10,
Size = new Vector2(icon_size),
Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
};
break;
case LabelStyles.Text:
labelEarly = new OsuSpriteText
{
Y = -10,
Text = "Early",
Font = OsuFont.Default.With(size: 10),
Height = 12,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
};
labelLate = new OsuSpriteText
{
Y = 10,
Text = "Late",
Font = OsuFont.Default.With(size: 10),
Height = 12,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
};
break;
default:
throw new ArgumentOutOfRangeException(nameof(style), style, null);
}
if (labelEarly != null)
{
colourBars.Add(labelEarly);
labelEarly.FadeInFromZero(500);
}
if (labelLate != null)
{
colourBars.Add(labelLate);
labelLate.FadeInFromZero(500);
}
}
protected override void Update()
@ -164,8 +336,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
base.Update();
// undo any layout rotation to display icons in the correct orientation
iconEarly.Rotation = -Rotation;
iconLate.Rotation = -Rotation;
if (labelEarly != null) labelEarly.Rotation = -Rotation;
if (labelLate != null) labelLate.Rotation = -Rotation;
}
private void createColourBars((HitResult result, double length)[] windows)
@ -224,11 +396,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
}
private double floatingAverage;
private Container colourBars;
private const int max_concurrent_judgements = 50;
protected override void OnNewJudgement(JudgementResult judgement)
{
const int arrow_move_duration = 800;
@ -255,6 +422,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
judgementsContainer.Add(new JudgementLine
{
JudgementLineThickness = { BindTarget = JudgementLineThickness },
Y = getRelativeJudgementPosition(judgement.TimeOffset),
Colour = GetColourForHitResult(judgement.Type),
});
@ -268,11 +436,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
internal class JudgementLine : CompositeDrawable
{
public readonly BindableNumber<float> JudgementLineThickness = new BindableFloat();
public JudgementLine()
{
RelativeSizeAxes = Axes.X;
RelativePositionAxes = Axes.Y;
Height = judgement_line_height;
Blending = BlendingParameters.Additive;
@ -295,6 +464,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Alpha = 0;
Width = 0;
JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true);
this
.FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint)
.ResizeWidthTo(1, judgement_fade_in_duration, Easing.OutQuint)
@ -306,5 +477,19 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
}
public override void Clear() => judgementsContainer.Clear();
public enum CentreMarkerStyles
{
None,
Circle,
Line
}
public enum LabelStyles
{
None,
Icons,
Text
}
}
}

View File

@ -84,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD
protected override bool OnMouseMove(MouseMoveEvent e)
{
positionalAdjust = Vector2.Distance(e.ScreenSpaceMousePosition, button.ScreenSpaceDrawQuad.Centre) / 200;
positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent)) / 100;
return base.OnMouseMove(e);
}

View File

@ -615,16 +615,22 @@ namespace osu.Game.Screens.Play
/// <param name="time">The destination time to seek to.</param>
internal void NonFrameStableSeek(double time)
{
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
frameStablePlaybackResetDelegate.RunTask();
// TODO: This schedule should not be required and is a temporary hotfix.
// See https://github.com/ppy/osu/issues/17267 for the issue.
// See https://github.com/ppy/osu/pull/17302 for a better fix which needs some more time.
ScheduleAfterChildren(() =>
{
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
frameStablePlaybackResetDelegate.RunTask();
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
DrawableRuleset.FrameStablePlayback = false;
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
DrawableRuleset.FrameStablePlayback = false;
Seek(time);
Seek(time);
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
});
}
/// <summary>

View File

@ -1,10 +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 enable
using System;
using System.Diagnostics;
using System.Threading.Tasks;
using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -48,31 +49,31 @@ namespace osu.Game.Screens.Play
public override bool HandlePositionalInput => true;
// We show the previous screen status
protected override UserActivity InitialActivity => null;
protected override UserActivity? InitialActivity => null;
protected override bool PlayResumeSound => false;
protected BeatmapMetadataDisplay MetadataInfo { get; private set; }
protected BeatmapMetadataDisplay MetadataInfo { get; private set; } = null!;
/// <summary>
/// A fill flow containing the player settings groups, exposed for the ability to hide it from inheritors of the player loader.
/// </summary>
protected FillFlowContainer<PlayerSettingsGroup> PlayerSettings { get; private set; }
protected FillFlowContainer<PlayerSettingsGroup> PlayerSettings { get; private set; } = null!;
protected VisualSettings VisualSettings { get; private set; }
protected VisualSettings VisualSettings { get; private set; } = null!;
protected AudioSettings AudioSettings { get; private set; }
protected AudioSettings AudioSettings { get; private set; } = null!;
protected Task LoadTask { get; private set; }
protected Task? LoadTask { get; private set; }
protected Task DisposalTask { get; private set; }
protected Task? DisposalTask { get; private set; }
private bool backgroundBrightnessReduction;
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
private AudioFilter lowPassFilter;
private AudioFilter highPassFilter;
private AudioFilter lowPassFilter = null!;
private AudioFilter highPassFilter = null!;
protected bool BackgroundBrightnessReduction
{
@ -90,47 +91,49 @@ namespace osu.Game.Screens.Play
private bool readyForPush =>
!playerConsumed
// don't push unless the player is completely loaded
&& player?.LoadState == LoadState.Ready
&& CurrentPlayer?.LoadState == LoadState.Ready
// don't push if the user is hovering one of the panes, unless they are idle.
&& (IsHovered || idleTracker.IsIdle.Value)
// don't push if the user is dragging a slider or otherwise.
&& inputManager?.DraggedDrawable == null
&& inputManager.DraggedDrawable == null
// don't push if a focused overlay is visible, like settings.
&& inputManager?.FocusedDrawable == null;
&& inputManager.FocusedDrawable == null;
private readonly Func<Player> createPlayer;
private Player player;
/// <summary>
/// The <see cref="Player"/> instance being loaded by this screen.
/// </summary>
public Player? CurrentPlayer { get; private set; }
/// <summary>
/// Whether the curent player instance has been consumed via <see cref="consumePlayer"/>.
/// Whether the current player instance has been consumed via <see cref="consumePlayer"/>.
/// </summary>
private bool playerConsumed;
private LogoTrackingContainer content;
private LogoTrackingContainer content = null!;
private bool hideOverlays;
private InputManager inputManager;
private InputManager inputManager = null!;
private IdleTracker idleTracker;
private IdleTracker idleTracker = null!;
private ScheduledDelegate scheduledPushPlayer;
private ScheduledDelegate? scheduledPushPlayer;
[CanBeNull]
private EpilepsyWarning epilepsyWarning;
private EpilepsyWarning? epilepsyWarning;
[Resolved(CanBeNull = true)]
private NotificationOverlay notificationOverlay { get; set; }
private NotificationOverlay? notificationOverlay { get; set; }
[Resolved(CanBeNull = true)]
private VolumeOverlay volumeOverlay { get; set; }
private VolumeOverlay? volumeOverlay { get; set; }
[Resolved]
private AudioManager audioManager { get; set; }
private AudioManager audioManager { get; set; } = null!;
[Resolved(CanBeNull = true)]
private BatteryInfo batteryInfo { get; set; }
private BatteryInfo? batteryInfo { get; set; }
public PlayerLoader(Func<Player> createPlayer)
{
@ -237,12 +240,14 @@ namespace osu.Game.Screens.Play
{
base.OnResuming(last);
var lastScore = player.Score;
Debug.Assert(CurrentPlayer != null);
var lastScore = CurrentPlayer.Score;
AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo;
// prepare for a retry.
player = null;
CurrentPlayer = null;
playerConsumed = false;
cancelLoad();
@ -344,9 +349,10 @@ namespace osu.Game.Screens.Play
private Player consumePlayer()
{
Debug.Assert(!playerConsumed);
Debug.Assert(CurrentPlayer != null);
playerConsumed = true;
return player;
return CurrentPlayer;
}
private void prepareNewPlayer()
@ -354,11 +360,11 @@ namespace osu.Game.Screens.Play
if (!this.IsCurrentScreen())
return;
player = createPlayer();
player.RestartCount = restartCount++;
player.RestartRequested = restartRequested;
CurrentPlayer = createPlayer();
CurrentPlayer.RestartCount = restartCount++;
CurrentPlayer.RestartRequested = restartRequested;
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false);
}
private void restartRequested()
@ -472,7 +478,7 @@ namespace osu.Game.Screens.Play
if (isDisposing)
{
// if the player never got pushed, we should explicitly dispose it.
DisposalTask = LoadTask?.ContinueWith(_ => player?.Dispose());
DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose());
}
}
@ -480,7 +486,7 @@ namespace osu.Game.Screens.Play
#region Mute warning
private Bindable<bool> muteWarningShownOnce;
private Bindable<bool> muteWarningShownOnce = null!;
private int restartCount;
@ -535,7 +541,7 @@ namespace osu.Game.Screens.Play
#region Low battery warning
private Bindable<bool> batteryWarningShownOnce;
private Bindable<bool> batteryWarningShownOnce = null!;
private void showBatteryWarningIfNeeded()
{

View File

@ -65,10 +65,12 @@ namespace osu.Game.Screens.Ranking.Expanded
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
string creator = metadata.Author.Username;
int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely();
var topStatistics = new List<StatisticDisplay>
{
new AccuracyStatistic(score.Accuracy),
new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out int missCount) || missCount == 0),
new ComboStatistic(score.MaxCombo, beatmapMaxCombo),
new PerformanceStatistic(score),
};
@ -80,8 +82,6 @@ namespace osu.Game.Screens.Ranking.Expanded
statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics);
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
AddInternal(new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
@ -224,6 +224,8 @@ namespace osu.Game.Screens.Ranking.Expanded
if (score.Date != default)
AddInternal(new PlayedOnText(score.Date));
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
if (starDifficulty != null)
{
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)

View File

@ -25,11 +25,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
/// Creates a new <see cref="ComboStatistic"/>.
/// </summary>
/// <param name="combo">The combo to be displayed.</param>
/// <param name="isPerfect">Whether this is a perfect combo.</param>
public ComboStatistic(int combo, bool isPerfect)
: base("combo", combo)
/// <param name="maxCombo">The maximum value of <paramref name="combo"/>.</param>
public ComboStatistic(int combo, int? maxCombo)
: base("combo", combo, maxCombo)
{
this.isPerfect = isPerfect;
isPerfect = combo == maxCombo;
}
public override void Appear()

View File

@ -51,7 +51,7 @@ namespace osu.Game.Skinning.Editor
fill.Clear();
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes()
.Where(t => !t.IsInterface)
.Where(t => !t.IsInterface && !t.IsAbstract)
.Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t))
.OrderBy(t => t.Name)
.ToArray();

View File

@ -19,15 +19,15 @@ namespace osu.Game.Skinning.Editor
/// A container which handles loading a skin editor on user request for a specified target.
/// This also handles the scaling / positioning adjustment of the target.
/// </summary>
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
public class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
{
private readonly ScalingContainer scalingContainer;
protected override bool BlockNonPositionalInput => true;
[CanBeNull]
private SkinEditor skinEditor;
public const float VISIBLE_TARGET_SCALE = 0.8f;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
@ -49,33 +49,13 @@ namespace osu.Game.Skinning.Editor
Hide();
return true;
case GlobalAction.ToggleSkinEditor:
Toggle();
return true;
}
return false;
}
public void Toggle()
protected override void PopIn()
{
if (skinEditor == null)
Show();
else
skinEditor.ToggleVisibility();
}
public override void Hide()
{
// base call intentionally omitted.
skinEditor?.Hide();
}
public override void Show()
{
// base call intentionally omitted as we have custom behaviour.
if (skinEditor != null)
{
skinEditor.Show();
@ -83,29 +63,24 @@ namespace osu.Game.Skinning.Editor
}
var editor = new SkinEditor();
editor.State.BindValueChanged(visibility => updateComponentVisibility());
skinEditor = editor;
// Schedule ensures that if `Show` is called before this overlay is loaded,
// it will not throw (LoadComponentAsync requires the load target to be in a loaded state).
Schedule(() =>
LoadComponentAsync(editor, _ =>
{
if (editor != skinEditor)
return;
LoadComponentAsync(editor, _ =>
{
if (editor != skinEditor)
return;
AddInternal(editor);
AddInternal(editor);
SetTarget(lastTargetScreen);
});
SetTarget(lastTargetScreen);
});
}
protected override void PopOut() => skinEditor?.Hide();
private void updateComponentVisibility()
{
Debug.Assert(skinEditor != null);

View File

@ -94,7 +94,7 @@ namespace osu.Game.Skinning.Editor
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
if (replayGeneratingMod != null)
screen.Push(new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)));
screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods))));
}, new[] { typeof(Player), typeof(SongSelect) })
},
}
@ -104,7 +104,7 @@ namespace osu.Game.Skinning.Editor
};
}
private class SceneButton : OsuButton
public class SceneButton : OsuButton
{
public SceneButton()
{

View File

@ -163,6 +163,12 @@ namespace osu.Game.Stores
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
}
protected override void UndeleteForReuse(BeatmapSetInfo existing)
{
base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow;
}
public override bool IsAvailableLocally(BeatmapSetInfo model)
{
return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));

View File

@ -351,7 +351,8 @@ namespace osu.Game.Stores
using (var transaction = realm.BeginWrite())
{
existing.DeletePending = false;
if (existing.DeletePending)
UndeleteForReuse(existing);
transaction.Commit();
}
@ -387,7 +388,9 @@ namespace osu.Game.Stores
{
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
existing.DeletePending = false;
if (existing.DeletePending)
UndeleteForReuse(existing);
transaction.Commit();
return existing.ToLive(Realm);
@ -527,6 +530,15 @@ namespace osu.Game.Stores
private bool checkAllFilesExist(TModel model) =>
model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath()));
/// <summary>
/// Called when an existing model is in a soft deleted state but being recovered.
/// </summary>
/// <param name="existing">The existing model.</param>
protected virtual void UndeleteForReuse(TModel existing)
{
existing.DeletePending = false;
}
/// <summary>
/// Whether this specified path should be removed after successful import.
/// </summary>

View File

@ -7,10 +7,13 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
@ -93,6 +96,10 @@ namespace osu.Game.Tests.Visual
protected class TestEditor : Editor
{
[Resolved(canBeNull: true)]
[CanBeNull]
private DialogOverlay dialogOverlay { get; set; }
public new void Undo() => base.Undo();
public new void Redo() => base.Redo();
@ -111,6 +118,18 @@ namespace osu.Game.Tests.Visual
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
public override bool OnExiting(IScreen next)
{
// For testing purposes allow the screen to exit without saving on second attempt.
if (!ExitConfirmed && dialogOverlay?.CurrentDialog is PromptForSaveDialog saveDialog)
{
saveDialog.PerformAction<PopupDialogDangerousButton>();
return true;
}
return base.OnExiting(next);
}
public TestEditor(EditorLoader loader = null)
: base(loader)
{

View File

@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Development;
using osu.Framework.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
@ -296,11 +297,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
return Task.CompletedTask;
}
private CancellationTokenSource? countdownFinishSource;
private CancellationTokenSource? countdownSkipSource;
private CancellationTokenSource? countdownStopSource;
private Task countdownTask = Task.CompletedTask;
public void FinishCountDown() => countdownFinishSource?.Cancel();
/// <summary>
/// Skips to the end of the currently-running countdown, if one is running,
/// and runs the callback (e.g. to start the match) as soon as possible unless the countdown has been cancelled.
/// </summary>
public void SkipToEndOfCountdown() => countdownSkipSource?.Cancel();
public override async Task SendMatchRequest(MatchUserRequest request)
{
@ -310,25 +315,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
switch (request)
{
case StartMatchCountdownRequest matchCountdownRequest:
Debug.Assert(ThreadSafety.IsUpdateThread);
countdownStopSource?.Cancel();
// Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental.
// If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly.
var stopSource = countdownStopSource = new CancellationTokenSource();
var finishSource = countdownFinishSource = new CancellationTokenSource();
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token);
var countdown = new MatchStartCountdown { EndTime = DateTimeOffset.Now + matchCountdownRequest.Delay };
var skipSource = countdownSkipSource = new CancellationTokenSource();
var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration };
Task lastCountdownTask = countdownTask;
countdownTask = start();
async Task start()
{
try
{
await lastCountdownTask;
}
catch (OperationCanceledException)
{
}
await lastCountdownTask;
Schedule(() =>
{
@ -341,10 +343,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
try
{
await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false);
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// Clients need to be notified of cancellations in the following code.
}
Schedule(() =>
@ -355,11 +359,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Room.Countdown = null;
MatchEvent(new CountdownChangedEvent { Countdown = null });
using (cancellationSource)
{
if (stopSource.Token.IsCancellationRequested)
return;
}
if (stopSource.IsCancellationRequested)
return;
StartMatch().WaitSafely();
});
@ -392,7 +393,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
public override async Task StartMatch()
public override Task StartMatch()
{
Debug.Assert(Room != null);
@ -400,7 +401,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
await ((IMultiplayerClient)this).LoadRequested();
return ((IMultiplayerClient)this).LoadRequested();
}
public override Task AbortGameplay()

View File

@ -88,7 +88,8 @@ namespace osu.Game.Tests.Visual.Spectator
/// </summary>
/// <param name="userId">The user to send frames for.</param>
/// <param name="count">The total number of frames to send.</param>
public void SendFramesFromUser(int userId, int count)
/// <param name="startTime">The time to start gameplay frames from.</param>
public void SendFramesFromUser(int userId, int count, double startTime = 0)
{
var frames = new List<LegacyReplayFrame>();
@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Spectator
flush();
var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1;
frames.Add(new LegacyReplayFrame(currentFrameIndex * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
frames.Add(new LegacyReplayFrame(currentFrameIndex * 100 + startTime, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
}
flush();

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
@ -82,7 +83,8 @@ namespace osu.Game.Updater
break;
case RuntimeInfo.Platform.macOS:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal));
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel";
bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal));
break;
case RuntimeInfo.Platform.Linux:

View File

@ -31,7 +31,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.1210.0">
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.320.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>