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

Compare commits

...

21 Commits

84 changed files with 2153 additions and 608 deletions
+31 -3
View File
@@ -50,7 +50,7 @@ jobs:
exit $exit_code
- name: InspectCode
uses: JetBrains/ReSharper-InspectCode@v0.11
uses: JetBrains/ReSharper-InspectCode@v0.12
with:
# this is WTF tier but if you don't specify *both* of these the defaults assume `build: true`
build: false
@@ -101,14 +101,42 @@ jobs:
NUnit.ConsoleOut=0
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
# https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#cancelled
- name: Upload Test Results
uses: actions/upload-artifact@v7
if: ${{ always() }}
if: ${{ !cancelled() }}
with:
name: osu-test-results-${{matrix.os.prettyname}}-${{matrix.threadingMode}}
path: ${{github.workspace}}/TestResults/TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx
test-results:
name: Test results
runs-on: ubuntu-latest
# we want to wait for the `test` job to complete, but run regardless of whether it succeeds or fails
# https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#example-not-requiring-successful-dependent-jobs
if: ${{ !cancelled() }}
needs: test
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Download results
uses: actions/download-artifact@v8
with:
pattern: osu-test-results-*
merge-multiple: true
- name: Add test results summary to workflow run
uses: dorny/test-reporter@v3.0.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
use-actions-summary: 'true'
build-only-android:
name: Build only (Android)
runs-on: windows-latest
-45
View File
@@ -1,45 +0,0 @@
# This is a workaround to allow PRs to report their coverage. This will run inside the base repository.
# See:
# * https://github.com/dorny/test-reporter#recommended-setup-for-public-repositories
# * https://docs.github.com/en/actions/reference/authentication-in-a-workflow#permissions-for-the-github_token
name: Annotate CI run with test results
on:
workflow_run:
workflows: [ "Continuous Integration" ]
types:
- completed
permissions:
contents: read
actions: read
checks: write
jobs:
annotate:
name: Annotate CI run with test results
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion != 'cancelled' }}
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
with:
repository: ${{ github.event.workflow_run.repository.full_name }}
ref: ${{ github.event.workflow_run.head_sha }}
- name: Download results
uses: actions/download-artifact@v8
with:
pattern: osu-test-results-*
merge-multiple: true
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Annotate CI run with test results
uses: dorny/test-reporter@v2.6.0
with:
name: Results
path: "*.trx"
reporter: dotnet-trx
list-suites: 'failed'
list-tests: 'failed'
+13
View File
@@ -13,6 +13,19 @@
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Debug, Second Client)",
"type": "coreclr",
"request": "launch",
"program": "dotnet",
"args": [
"${workspaceRoot}/osu.Desktop/bin/Debug/net8.0/osu!.dll",
"--debug-client-id=1"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": "Build osu! (Debug)",
"console": "internalConsole"
},
{
"name": "osu! (Release)",
"type": "coreclr",
+1
View File
@@ -155,6 +155,7 @@ namespace osu.Game.Rulesets.Catch
new CatchModMuted(),
new CatchModNoScope(),
new CatchModMovingFast(),
new CatchModSynesthesia(),
};
case ModType.System:
@@ -0,0 +1,60 @@
// 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.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Screens.Edit;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Catch.Mods
{
/// <summary>
/// Mod that colours <see cref="HitObject"/>s based on the musical division they are on
/// </summary>
public class CatchModSynesthesia : ModSynesthesia, IApplicableToBeatmap, IApplicableToDrawableHitObject
{
private readonly OsuColour colours = new OsuColour();
private IBeatmap? currentBeatmap { get; set; }
public void ApplyToBeatmap(IBeatmap beatmap)
{
//Store a reference to the current beatmap to look up the beat divisor when notes are drawn
if (currentBeatmap != beatmap)
currentBeatmap = beatmap;
}
public void ApplyToDrawableHitObject(DrawableHitObject d)
{
if (currentBeatmap == null) return;
Color4? timingBasedColour = null;
d.HitObjectApplied += _ =>
{
// Block bananas from getting coloured.
if (d.HitObject is not Banana)
{
timingBasedColour = BindableBeatDivisor.GetColourFor(currentBeatmap.ControlPointInfo.GetClosestBeatDivisor(d.HitObject.StartTime), colours);
}
// Colour droplets into a solid colour, as droplets aren't generated snapped to timeline ticks.
if (d.HitObject is Droplet)
{
timingBasedColour = Color4.LightGreen;
}
};
// Need to set this every update to ensure it doesn't get overwritten by DrawableHitObject.OnApply() -> UpdateComboColour().
d.OnUpdate += _ =>
{
if (timingBasedColour != null)
d.AccentColour.Value = timingBasedColour.Value;
};
}
}
}
@@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.Edit
protected override void Update()
{
TimeRange.Value = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<double>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
base.Update();
}
}
@@ -141,6 +141,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public void MissForcefully() => ApplyMinResult();
// ReSharper disable once FunctionRecursiveOnAllPaths (TODO: remove after fixed https://youtrack.jetbrains.com/issue/RIDER-135036/Incorrect-recursive-on-all-execution-paths-inspection)
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;
/// <summary>
@@ -65,6 +65,7 @@ namespace osu.Game.Tests.Beatmaps
}
[Test]
[FlakyTest] // one fix attempted in https://github.com/ppy/osu/pull/37178, didn't work
public void TestInvalidationFlow()
{
BeatmapInfo postEditBeatmapInfo = null;
@@ -187,6 +187,7 @@ namespace osu.Game.Tests.Database
}
[Test]
[FlakyTest]
public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
{
RulesetInfo rulesetInfo = null!;
@@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[FlakyTest]
public void TestLengthAndStarRatingUpdated()
{
WorkingBeatmap working = null;
@@ -24,6 +24,7 @@ namespace osu.Game.Tests.Visual.Editing
}
[Test]
[FlakyTest]
public void TestLocallyModifyingOnlineBeatmap()
{
string initialHash = string.Empty;
@@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
@@ -140,14 +139,16 @@ namespace osu.Game.Tests.Visual.Editing
private void setUpEditor(RulesetInfo ruleset)
{
BeatmapSetInfo beatmapSet = null!;
BeatmapSetInfo? beatmapSet = null;
AddStep("Import test beatmap", () =>
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
);
AddStep("Retrieve beatmap", () =>
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
);
AddUntilStep("Retrieve beatmap", () =>
{
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected)?.Value.Detach();
return beatmapSet != null;
});
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
AddUntilStep("Wait for song select", () =>
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
@@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
AddStep("Open editor for ruleset", () =>
((SoloSongSelect)Game.ScreenStack.CurrentScreen)
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
.Edit(beatmapSet!.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
);
AddUntilStep("Wait for editor open", () => editor?.ReadyForUse == true);
}
@@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private bool seek;
[Test]
[FlakyTest]
[Ignore("Still failing even with [FlakyTest] applied.")]
public void TestAllSamplesStopDuringSeek()
{
DrawableSlider? slider = null;
@@ -211,7 +211,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("move mouse to centre of screen", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.Centre));
AddUntilStep("wait for settings overlay hidden", () => settingsOverlay().Expanded.Value, () => Is.False);
PlayerSettingsOverlay settingsOverlay() => Player.ChildrenOfType<PlayerSettingsOverlay>().Single();
ReplaySettingsOverlay settingsOverlay() => Player.ChildrenOfType<ReplaySettingsOverlay>().Single();
}
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
@@ -331,7 +331,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
p.RequestResults = _ => resultsRequested = true;
});
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any() && playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddUntilStep("wait for load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any()
&& playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().Any()
&& playlist.ChildrenOfType<BeatmapCardThumbnail>().First().DrawWidth > 0);
AddStep("move mouse to first item title", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<LinkFlowContainer>().First().ChildrenOfType<SpriteText>().First()));
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
@@ -50,47 +50,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.CacheAs(ongoingOperationTracker = new OngoingOperationTracker());
Dependencies.CacheAs(availabilityTracker.Object);
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest:
clearRoomCountdown();
break;
}
});
Children = new Drawable[]
{
ongoingOperationTracker,
@@ -103,10 +62,51 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("reset state", () =>
{
multiplayerClient.Invocations.Clear();
multiplayerClient.Reset();
multiplayerClient.SetupGet(m => m.LocalUser).Returns(() => localUser);
multiplayerClient.SetupGet(m => m.Room).Returns(() => multiplayerRoom);
// By default, the local user is to be the host.
multiplayerClient.SetupGet(m => m.IsHost).Returns(() => ReferenceEquals(multiplayerRoom.Host, localUser));
// Assume all state changes are accepted by the server.
multiplayerClient.Setup(m => m.ChangeState(It.IsAny<MultiplayerUserState>()))
.Callback((MultiplayerUserState r) =>
{
Logger.Log($"Changing local user state from {localUser.State} to {r}");
localUser.State = r;
raiseRoomUpdated();
});
multiplayerClient.Setup(m => m.StartMatch())
.Callback(() =>
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
// immediately "end" gameplay, as we don't care about that part of the process.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
});
multiplayerClient.Setup(m => m.SendMatchRequest(It.IsAny<MatchUserRequest>()))
.Callback((MatchUserRequest request) =>
{
switch (request)
{
case StartMatchCountdownRequest countdownStart:
setRoomCountdown(countdownStart.Duration);
break;
case StopCountdownRequest:
clearRoomCountdown();
break;
}
});
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
availabilityTracker.Reset();
availabilityTracker.SetupGet(a => a.Availability).Returns(beatmapAvailability);
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
@@ -375,6 +375,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAbortMatch()
{
setUpMatchCallbacks();
// Ready
ClickButtonWhenEnabled<MultiplayerReadyButton>();
// Start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// Abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
private void setUpMatchCallbacks()
{
AddStep("setup client", () =>
{
@@ -383,6 +399,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
multiplayerClient.Raise(m => m.LoadRequested -= null);
multiplayerClient.Object.Room!.State = MultiplayerRoomState.WaitingForLoad;
raiseRoomUpdated();
// The local user state doesn't really matter, so let's do the same as the base implementation for these tests.
changeUserState(localUser.UserID, MultiplayerUserState.Idle);
@@ -395,19 +412,133 @@ namespace osu.Game.Tests.Visual.Multiplayer
raiseRoomUpdated();
});
});
}
// Ready
ClickButtonWhenEnabled<MultiplayerReadyButton>();
[Test]
public void TestRefereeSpectating()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
// Start match
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("move to spectate", () => changeUserState(multiplayerClient.Object.LocalUser!.UserID, MultiplayerUserState.Spectating));
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
// start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// Abort
// abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
[Test]
public void TestRefereeFlowWithoutCountdown()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
// start match
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddUntilStep("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
// abort
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("check abort request received", () => multiplayerClient.Verify(m => m.AbortMatch(), Times.Once));
}
[Test]
public void TestRefereeFlowWithCountdown()
{
AddStep("set up referee", () =>
{
multiplayerClient.SetupGet(m => m.IsReferee).Returns(true);
multiplayerClient.SetupGet(m => m.IsHost).Returns(false);
multiplayerClient.Object.Room!.Users.Single().Role = MultiplayerRoomUserRole.Referee;
raiseRoomUpdated();
});
const int users = 10;
AddStep("add many users", () =>
{
for (int i = 0; i < users; i++)
addUser(new APIUser { Id = i, Username = "Another user" });
});
AddAssert("button disabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.False);
AddStep("ready up a user", () => changeUserState(9, MultiplayerUserState.Ready));
AddAssert("button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value, () => Is.True);
setUpMatchCallbacks();
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the first countdown button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.Is<StartMatchCountdownRequest>(req =>
req.Duration == TimeSpan.FromSeconds(10)
)), Times.Once);
});
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
AddStep("click the cancel button", () =>
{
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().Last();
InputManager.MoveMouseTo(popoverButton);
InputManager.Click(MouseButton.Left);
});
AddStep("check request received", () =>
{
multiplayerClient.Verify(m => m.SendMatchRequest(It.IsAny<StopCountdownRequest>()), Times.Once);
});
}
private void verifyGameplayStartFlow()
{
checkLocalUserState(MultiplayerUserState.Ready);
@@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
// components wrapped in skinnable target containers load asynchronously, potentially taking more than one frame to load.
// therefore use until step rather than direct assert to account for that.
AddUntilStep("all interactive elements removed", () => this.ChildrenOfType<Player>().All(p =>
!p.ChildrenOfType<PlayerSettingsOverlay>().Any() &&
!p.ChildrenOfType<ReplaySettingsOverlay>().Any() &&
!p.ChildrenOfType<HoldForMenuButton>().Any() &&
p.ChildrenOfType<ArgonSongProgressBar>().SingleOrDefault()?.Interactive == false));
@@ -662,7 +662,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("invoke on back button", () => multiplayerComponents.OnBackButton());
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().Single().State.Value == Visibility.Hidden);
AddAssert("mod overlay is hidden", () => this.ChildrenOfType<MultiplayerUserModSelectOverlay>().All(o => o.State.Value == Visibility.Hidden));
AddAssert("dialog overlay is hidden", () => DialogOverlay.State.Value == Visibility.Hidden);
@@ -353,6 +353,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
}
[Test]
public void TestChangeSettingsButtonAlwaysVisibleForReferee()
{
AddStep("add playlist item", () =>
{
room.Playlist =
[
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
});
AddStep("setup referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
AddUntilStep("button visible", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
}
[Test]
public void TestUserModSelectUpdatesWhenNotVisible()
{
@@ -32,10 +32,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
public override void SetUpSteps()
private void setUpList()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
createNewParticipantsList();
@@ -44,6 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAddUser()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
@@ -59,6 +58,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAddReferee()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(3)
@@ -78,6 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestAddUnresolvedUser()
{
setUpList();
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
AddStep("add non-resolvable user", () => MultiplayerClient.TestAddUnresolvedUser());
@@ -94,6 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRemoveUser()
{
setUpList();
APIUser? secondUser = null;
AddStep("add a user", () =>
@@ -114,6 +117,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGameStateHasPriorityOverDownloadState()
{
setUpList();
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
@@ -128,6 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCorrectInitialState()
{
setUpList();
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
createNewParticipantsList();
checkProgressBarVisibility(true);
@@ -136,6 +141,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestBeatmapDownloadingStates()
{
setUpList();
AddStep("set to unknown", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Unknown()));
AddStep("set to no map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set to downloading map", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
@@ -159,6 +165,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestToggleReadyState()
{
setUpList();
AddAssert("ready mark invisible", () => !this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => MultiplayerClient.ChangeState(MultiplayerUserState.Ready));
@@ -171,6 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestToggleSpectateState()
{
setUpList();
AddStep("make user spectating", () => MultiplayerClient.ChangeState(MultiplayerUserState.Spectating));
AddStep("make user idle", () => MultiplayerClient.ChangeState(MultiplayerUserState.Idle));
}
@@ -178,6 +186,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCrownChangesStateWhenHostTransferred()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -201,6 +210,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestHostGetsPinnedToTop()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -218,8 +228,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
[Test]
public void TestKickButtonOnlyPresentWhenHost()
public void TestKickButtonPresentWhenHost()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -238,9 +249,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonPresentWhenReferee()
{
AddStep("set up referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
Username = "Second",
CoverUrl = TestResources.COVER_IMAGE_3,
}));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
AddUntilStep("kick buttons visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 1);
}
[Test]
public void TestKickButtonKicks()
{
setUpList();
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
{
Id = 3,
@@ -258,6 +293,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
const int users_count = 200;
setUpList();
AddStep("add many users", () =>
{
for (int i = 0; i < users_count; i++)
@@ -316,6 +352,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestUserWithMods()
{
setUpList();
AddStep("add user", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -353,6 +390,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestUserWithStyle()
{
setUpList();
AddStep("add users", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -380,6 +418,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestModOverlap()
{
setUpList();
AddStep("add dummy mods", () =>
{
MultiplayerClient.ChangeUserMods(new Mod[]
@@ -438,6 +477,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestModsAndRuleset()
{
setUpList();
AddStep("add another user", () =>
{
MultiplayerClient.AddUser(new APIUser
@@ -472,6 +512,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestTeams()
{
setUpList();
AddStep("enable teams", () => MultiplayerClient.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("one unique panel", () => this.ChildrenOfType<ParticipantPanel>().Select(p => p.Current.Value).Distinct().Count() == 1);
@@ -41,10 +41,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(Realm);
}
public override void SetUpSteps()
private void setUpRoom()
{
base.SetUpSteps();
AddStep("create room", () => room = CreateDefaultRoom());
AddStep("join room", () => JoinRoom(room));
WaitForJoined();
@@ -80,6 +78,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDeleteButtonAlwaysVisibleForHost()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -92,6 +92,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -108,9 +110,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestDeleteButtonAlwaysVisibleForReferee()
{
AddStep("ensure host will be referee", () => MultiplayerClient.RoomSetupAction = r => r.Host!.Role = MultiplayerRoomUserRole.Referee);
setUpRoom();
AddStep("join other user", () => MultiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(1, true);
addPlaylistItem(() => 1234);
assertDeleteButtonVisibility(2, true);
AddStep("set host only queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.HostOnly }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.HostOnly);
AddStep("set other user as host", () => MultiplayerClient.TransferHost(1234));
assertDeleteButtonVisibility(1, true);
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestSingleItemDoesNotHaveDeleteButton()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -120,6 +148,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestCurrentItemHasDeleteButtonIfNotSingle()
{
setUpRoom();
AddStep("set all players queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }).WaitSafely());
AddUntilStep("wait for queue mode change", () => MultiplayerClient.ClientAPIRoom?.QueueMode == QueueMode.AllPlayers);
@@ -139,6 +169,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestChangeExistingItem()
{
setUpRoom();
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
{
ID = playlist.Items[0].ID,
@@ -472,7 +472,7 @@ namespace osu.Game.Tests.Visual.Navigation
void checkScrollSpeed(double configValue, double gameplayValue)
{
AddUntilStep($"config value is {configValue}", () => getConfigManager().Get<double>(ManiaRulesetSetting.ScrollSpeed), () => Is.EqualTo(configValue));
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().ScrollingInfo.TimeRange.Value,
AddUntilStep($"gameplay value is {gameplayValue}", () => this.ChildrenOfType<DrawableManiaRuleset>().Single().TargetTimeRange,
() => Is.EqualTo(DrawableManiaRuleset.ComputeScrollTime(gameplayValue)));
}
@@ -653,6 +653,7 @@ namespace osu.Game.Tests.Visual.Navigation
}
[Test]
[FlakyTest]
public void TestDeleteScoreAfterPlaying()
{
playToResults();
@@ -300,7 +300,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
ReplaySettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<ReplaySettingsOverlay>().SingleOrDefault();
}
[Test]
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Graphics.Containers.Markdown.Footnotes;
using osu.Game.Overlays;
@@ -143,98 +142,11 @@ outdated: true # not sure about the format for ""list of mods"".
AddAssert("No notice box visible", () => !markdownContainer.ChildrenOfType<Container>().Any());
}
[Test]
public void TestAbsoluteImage()
{
AddStep("Add absolute image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/images/Client/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestRelativeImage()
{
AddStep("Add relative image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "![intro](../images/Client/Interface/img/intro-screen.jpg)";
});
}
[Test]
public void TestBlockImage()
{
AddStep("Add paragraph with block image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image
![play menu](../images/Client/Interface/img/play-menu.jpg ""Main Menu in osu!"")
Line after image";
});
}
[Test]
public void TestInlineImage()
{
AddStep("Add inline image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![osu! mode icon](/wiki/shared/mode/osu.png) osu!";
});
}
[Test]
public void TestTableWithImageContent()
{
AddStep("Add Table", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = @"
| Image | Name | Effect |
| :-: | :-: | :-- |
| ![](/wiki/images/shared/judgement/osu!/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
| ![](/wiki/images/shared/judgement/osu!/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
| ![](/wiki/images/shared/judgement/osu!/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
| ![](/wiki/images/shared/judgement/osu!/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
| ![](/wiki/images/shared/judgement/osu!/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
| ![](/wiki/images/shared/judgement/osu!/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
";
});
}
[Test]
public void TestWideImageNotExceedContainer()
{
AddStep("Add image", () =>
{
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
markdownContainer.Text = "![](../images/Client/Program_files/img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
});
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
AddStep("Change container width", () =>
{
markdownContainer.Width = 0.5f;
});
AddAssert("Image not exceed container width", () =>
{
var spriteImage = markdownContainer.ChildrenOfType<Sprite>().First();
return Precision.DefinitelyBigger(markdownContainer.DrawWidth, spriteImage.DrawWidth);
});
}
[Test]
public void TestFlag()
{
AddStep("Add flag", () =>
{
markdownContainer.CurrentPath = @"https://dev.ppy.sh";
markdownContainer.Text = "::{flag=\"AU\"}:: ::{flag=\"ZZ\"}::";
});
AddAssert("Two flags visible", () => markdownContainer.ChildrenOfType<DrawableFlag>().Count(), () => Is.EqualTo(2));
@@ -75,14 +75,14 @@ namespace osu.Game.Tests.Visual.RankedPlay
}
private double flushInterval = 1000;
private double recordInterval = 25;
private double recordInterval = 50;
private double fixedLatency;
private double maxLatency;
[Test]
public void TestCardHandReplay()
{
AddSliderStep("record interval", 0.0, 1000.0, 25.0, value =>
AddSliderStep("record interval", 0.0, 1000.0, 50.0, value =>
{
recordInterval = value;
recreateRecorder();
@@ -6,6 +6,7 @@ using Humanizer;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
@@ -33,12 +34,23 @@ namespace osu.Game.Tests.Visual.RankedPlay
};
}
[SetUpSteps]
public void SetupSteps()
{
AddStep("reset card hand", () => Child = handOfCards = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
});
}
[Test]
public void TestSingleSelectionMode()
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -59,7 +71,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -84,7 +95,13 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddStep($"{i} {"cards".Pluralize(i == 1)}", () =>
{
handOfCards.Clear();
Child = handOfCards = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
};
for (int j = 0; j < numCards; j++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
@@ -138,7 +155,6 @@ namespace osu.Game.Tests.Visual.RankedPlay
{
AddStep("add cards", () =>
{
handOfCards.Clear();
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
@@ -157,5 +173,24 @@ namespace osu.Game.Tests.Visual.RankedPlay
AddAssert("card selected", () => handOfCards.Selection.Contains(handOfCards.Cards.ElementAt(i1).Card.Item));
}
}
[Test]
public void TestContract()
{
AddStep("add cards", () =>
{
for (int i = 0; i < 5; i++)
handOfCards.AddCard(new RankedPlayCardWithPlaylistItem(new RankedPlayCardItem()));
});
AddWaitStep("wait", 5);
AddStep("contract", () => handOfCards.Contract());
AddWaitStep("wait", 5);
AddAssert(
"all cards outside bounds", () =>
handOfCards
.ChildrenOfType<HandOfCards.HandCard>()
.All(card => !card.ScreenSpaceDrawQuad.AABBFloat.IntersectsWith(handOfCards.ScreenSpaceDrawQuad.AABBFloat))
);
}
}
}
@@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -27,7 +28,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
}
@@ -35,7 +36,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
{
State = { BindTarget = visibility },
Child = new RankedPlayUserDisplay(2, Anchor.TopRight, RankedPlayColourScheme.Red)
Child = new RankedPlayUserDisplay(new APIUser { Id = 2, Username = "peppy" }, Anchor.TopRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
}
@@ -0,0 +1,91 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
namespace osu.Game.Tests.Visual.RankedPlay
{
public partial class TestSceneRankedPlayStageOverlay : RankedPlayTestScene
{
private Container content = null!;
protected override Container<Drawable> Content => content;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create components", () => base.Content.Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new RankedPlayBackground
{
RelativeSizeAxes = Axes.Both,
},
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
}
});
}
[Test]
public void TestBasic()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
[Test]
public void TestLongUsername()
{
AddStep("create", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 226597,
Username = "WWWWWWWWWWWWWWWWWWWW",
},
Multiplier = 2,
});
}
[Test]
public void TestColourScheme()
{
AddStep("create blue", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Blue)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
AddStep("create red", () => Child = new RankedPlayStageOverlay("Pick Phase", RankedPlayColourScheme.Red)
{
PickingUser = new APIUser
{
Id = 2,
Username = "peppy",
},
Multiplier = 2,
});
}
}
}
@@ -4,6 +4,9 @@
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Utils;
using osu.Game.Online.Rooms;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components;
using osu.Game.Tests.Visual.Multiplayer;
@@ -20,11 +23,19 @@ namespace osu.Game.Tests.Visual.RankedPlay
Value = 1_000_000,
};
public TestSceneRankedPlayUserDisplay()
{
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("add display", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.RankedPlay)));
WaitForJoined();
AddStep("add display", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -36,7 +47,7 @@ namespace osu.Game.Tests.Visual.RankedPlay
[Test]
public void TesUserDisplay()
{
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
AddStep("blue color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -44,15 +55,30 @@ namespace osu.Game.Tests.Visual.RankedPlay
Health = { BindTarget = health }
});
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(2, Anchor.BottomLeft, RankedPlayColourScheme.Red)
AddStep("red color scheme", () => Child = new RankedPlayUserDisplay(new APIUser { Id = 1001, Username = "User 1001" }, Anchor.BottomLeft, RankedPlayColourScheme.Red)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(256, 72),
Health = { BindTarget = health }
});
}
AddSliderStep("health", 0, 1_000_000, 1_000_000, value => health.Value = value);
[Test]
public void TestBeatmapState()
{
float progress = 0;
AddStep("set unavailable", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set downloading", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress = 0)));
AddUntilStep("increment progress", () =>
{
progress += RNG.NextSingle(0.1f);
MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress));
return progress >= 1;
});
AddStep("set to importing", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
AddStep("set to available", () => MultiplayerClient.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
}
}
}
@@ -277,13 +277,18 @@ namespace osu.Game.Tests.Visual.Ranking
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
AddUntilStep("retrieve expanded panel",
() => expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded),
() => Is.Not.Null);
AddUntilStep("retrieve contracted panel",
() => contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X),
() => Is.Not.Null);
AddStep("click expanded panel then contracted panel", () =>
{
expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
InputManager.MoveMouseTo(contractedPanel);
InputManager.Click(MouseButton.Left);
});
@@ -132,6 +132,7 @@ namespace osu.Game.Tests.Visual.Ranking
}
[Test]
[FlakyTest]
public void TestOnlineLeaderboardWithLessThan50Scores()
{
ScoreInfo localScore = null!;
@@ -34,11 +34,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddBeatmaps(10, 3);
WaitForDrawablePanels();
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(1));
ApplyToFilterAndWaitForFilter("filter", c => c.SearchText = BeatmapSets[2].Metadata.Title);
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(2));
CheckDisplayedBeatmapSetsCount(1);
CheckDisplayedBeatmapsCount(3);
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.SongSelect
ApplyToFilterAndWaitForFilter("remove filter", c => c.SearchText = string.Empty);
AddAssert("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
AddUntilStep("invocation count correct", () => NewItemsPresentedInvocationCount, () => Is.EqualTo(3));
CheckDisplayedBeatmapSetsCount(10);
CheckDisplayedBeatmapsCount(30);
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect
@@ -37,7 +38,11 @@ namespace osu.Game.Tests.Visual.SongSelect
WaitForFiltering();
AddAssert("select screen position unchanged", () => Carousel.ChildrenOfType<PanelBeatmap>().Single(p => p.Selected.Value).ScreenSpaceDrawQuad,
() => Is.EqualTo(positionBefore));
() => Is.EqualTo(positionBefore).Using<Quad, Quad>((expected, actual)
=> Precision.AlmostEquals(expected.TopLeft, actual.TopLeft)
&& Precision.AlmostEquals(expected.TopRight, actual.TopRight)
&& Precision.AlmostEquals(expected.BottomLeft, actual.BottomLeft)
&& Precision.AlmostEquals(expected.BottomRight, actual.BottomRight)));
}
[Test]
@@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestSetTraversal()
{
AddBeatmaps(3, splitApart: true);
@@ -127,6 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect
}
[Test]
[FlakyTest]
public void TestBestRulesetIsRecommended()
{
BeatmapSetInfo osuSet = null, mixedSet = null;
@@ -11,6 +11,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods;
@@ -23,6 +24,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Leaderboards;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Resources;
using osuTK.Input;
using BeatmapCarousel = osu.Game.Screens.Select.BeatmapCarousel;
@@ -430,6 +432,31 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("osu! cookie visible", () => this.ChildrenOfType<OsuLogo>().Single().Alpha, () => Is.Not.Zero);
}
[Test]
public void TestDropdownKeyboardNavigation()
{
ImportBeatmapForRuleset(0);
LoadSongSelect();
BeatmapInfo? firstBeatmap = null;
AddStep("store first difficulty", () => firstBeatmap = Beatmap.Value.BeatmapInfo);
AddStep("click sort dropdown", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<ShearedDropdown<SortMode>>().Single());
InputManager.Click(MouseButton.Left);
});
AddStep("press up arrow", () => InputManager.Key(Key.Up));
AddStep("press up arrow", () => InputManager.Key(Key.Up));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("sort mode is length", () => this.ChildrenOfType<ShearedDropdown<SortMode>>().Single().Current.Value, () => Is.EqualTo(SortMode.Length));
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo, () => Is.EqualTo(firstBeatmap));
}
#endregion
#region Footer
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@@ -11,13 +9,11 @@ using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
@@ -35,18 +31,17 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneDeleteLocalScore : OsuManualInputManagerTestScene
{
private readonly ContextMenuContainer contextMenuContainer;
private readonly BeatmapLeaderboardWedge leaderboard;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
private BeatmapManager beatmapManager = null!;
private ScoreManager scoreManager = null!;
private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
private BeatmapInfo beatmapInfo;
private BeatmapInfo beatmapInfo = null!;
private LeaderboardManager leaderboardManager { get; set; }
private LeaderboardManager leaderboardManager { get; set; } = null!;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
@@ -60,15 +55,11 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Children = new Drawable[]
{
contextMenuContainer = new OsuContextMenuContainer
leaderboard = new BeatmapLeaderboardWedge
{
RelativeSizeAxes = Axes.Both,
Child = leaderboard = new BeatmapLeaderboardWedge
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
}
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(0.6f),
},
dialogOverlay = new DialogOverlay()
};
@@ -145,7 +136,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDeleteViaRightClick()
{
ScoreInfo scoreBeingDeleted = null;
ScoreInfo scoreBeingDeleted = null!;
AddStep("open menu for top score", () =>
{
var leaderboardScore = leaderboard.ChildrenOfType<BeatmapLeaderboardScore>().First();
@@ -157,12 +148,12 @@ namespace osu.Game.Tests.Visual.UserInterface
});
// Ensure the context menu has finished showing
AddStep("finish transforms", () => contextMenuContainer.FinishTransforms(true));
AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
AddStep("click delete option", () =>
{
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>()
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<DrawableOsuMenuItem>()
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.Click(MouseButton.Left);
});
+4
View File
@@ -22,6 +22,8 @@ namespace osu.Game.Audio
protected TrackManagerPreviewTrack? CurrentTrack;
public readonly BindableBool IsPlayingPreview = new BindableBool();
public PreviewTrackManager(IAdjustableAudioComponent mainTrackAdjustments)
{
this.mainTrackAdjustments = mainTrackAdjustments;
@@ -47,6 +49,7 @@ namespace osu.Game.Audio
CurrentTrack?.Stop();
CurrentTrack = track;
mainTrackAdjustments.AddAdjustment(AdjustableProperty.Volume, muteBindable);
IsPlayingPreview.Value = true;
});
track.Stopped += () => Schedule(() =>
@@ -56,6 +59,7 @@ namespace osu.Game.Audio
CurrentTrack = null;
mainTrackAdjustments.RemoveAdjustment(AdjustableProperty.Volume, muteBindable);
IsPlayingPreview.Value = false;
});
return track;
+39 -16
View File
@@ -137,10 +137,15 @@ namespace osu.Game.Beatmaps
Value = new StarDifficulty(beatmapInfo.StarRating, 0)
};
updateBindable(bindable, currentRuleset.Value, currentMods.Value, cancellationToken, computationDelay);
lock (bindableUpdateLock)
{
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, cancellationToken);
linkedCancellationSources.Add(linkedSource);
updateBindable(bindable, currentRuleset.Value, currentMods.Value, linkedSource, computationDelay);
trackedBindables.Add(bindable);
}
return bindable;
}
@@ -212,7 +217,7 @@ namespace osu.Game.Beatmaps
var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken);
linkedCancellationSources.Add(linkedSource);
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource);
}
}
}
@@ -243,27 +248,45 @@ namespace osu.Game.Beatmaps
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
/// <param name="rulesetInfo">The <see cref="IRulesetInfo"/> to update with.</param>
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
/// <param name="linkedCancellationTokenSource">
/// A cancellation token source that may be used to cancel this update.
/// This token will be cancelled in one of two scenarios:
/// <list type="bullet">
/// <item>The owner of the bindable has requested the cancellation.</item>
/// <item>An <see cref="Invalidate"/> call has been issued, and as such ongoing calculations must be aborted to avoid stale values being potentially written to bindables.</item>
/// </list>
/// </param>
/// <param name="computationDelay">In the case a cached lookup was not possible, a value in milliseconds of to wait until performing potentially intensive lookup.</param>
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationToken cancellationToken = default, int computationDelay = 0)
private void updateBindable(BindableStarDifficulty bindable, IRulesetInfo? rulesetInfo, IEnumerable<Mod>? mods, CancellationTokenSource linkedCancellationTokenSource, int computationDelay = 0)
{
// GetDifficultyAsync will fall back to existing data from IBeatmapInfo if not locally available
// (contrary to GetAsync)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, cancellationToken, computationDelay)
GetDifficultyAsync(bindable.BeatmapInfo, rulesetInfo, mods, linkedCancellationTokenSource.Token, computationDelay)
.ContinueWith(task =>
{
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule(() =>
{
if (cancellationToken.IsCancellationRequested)
return;
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule(() =>
{
if (!linkedCancellationTokenSource.IsCancellationRequested)
{
StarDifficulty? starDifficulty = task.GetResultSafely();
StarDifficulty? starDifficulty = task.GetResultSafely();
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
}
if (starDifficulty != null)
bindable.Value = starDifficulty.Value;
});
}, cancellationToken);
// Once the linked cancellation token source is of no remaining use to anybody, clean it up.
lock (bindableUpdateLock)
{
linkedCancellationSources.Remove(linkedCancellationTokenSource);
linkedCancellationTokenSource.Dispose();
}
});
},
// This continuation MUST run even if the antecedent `GetDifficultyAsync()` call was canceled in order to clean up `linkedCancellationTokenSource`.
// Due to this, `ContinueWith()` CANNOT accept `linkedCancellationTokenSource.Token` here, because if it did, then in an event of a cancellation,
// the continuation would never be scheduled for execution.
CancellationToken.None);
}
/// <summary>
+30 -1
View File
@@ -54,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface
#region OsuDropdownMenu
public partial class OsuDropdownMenu : DropdownMenu
public partial class OsuDropdownMenu : DropdownMenu, IKeyBindingHandler<GlobalAction>
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
@@ -163,6 +163,35 @@ namespace osu.Game.Graphics.UserInterface
protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction);
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
// logic copied from https://github.com/ppy/osu-framework/blob/baf865f1fd9e677310e7e432a7c6af99db7db914/osu.Framework/Graphics/UserInterface/Dropdown.cs#L702-L717
var visibleMenuItemsList = VisibleMenuItems.ToList();
if (visibleMenuItemsList.Count > 0)
{
var currentPreselected = PreselectedItem;
int targetPreselectionIndex = visibleMenuItemsList.IndexOf(currentPreselected);
switch (e.Action)
{
case GlobalAction.SelectPrevious:
PreselectItem(targetPreselectionIndex - 1);
return true;
case GlobalAction.SelectNext:
PreselectItem(targetPreselectionIndex + 1);
return true;
}
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
#region DrawableOsuDropdownMenuItem
public partial class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem
@@ -196,6 +196,11 @@ namespace osu.Game.Online.Multiplayer
}
}
/// <summary>
/// Whether the <see cref="LocalUser"/> is a referee in the <see cref="Room"/>.
/// </summary>
public virtual bool IsReferee => LocalUser?.Role == MultiplayerRoomUserRole.Referee;
[Resolved]
protected IAPIProvider API { get; private set; } = null!;
@@ -92,7 +92,7 @@ namespace osu.Game.Online.Multiplayer
/// Determines whether a user is able to add playlist items to this room.
/// </summary>
/// <param name="user">The user to check.</param>
public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || Settings.QueueMode != QueueMode.HostOnly;
public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || user.Role == MultiplayerRoomUserRole.Referee || Settings.QueueMode != QueueMode.HostOnly;
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
}
@@ -3,6 +3,7 @@
using System;
using MessagePack;
using osuTK;
namespace osu.Game.Online.RankedPlay
{
@@ -18,5 +19,28 @@ namespace osu.Game.Online.RankedPlay
[Key(2)]
public required bool Selected { get; init; }
[Key(3)]
public required bool Dragged { get; init; }
[Key(4)]
public required int Order { get; init; }
[Key(5)]
public float DragX { get; init; }
[Key(6)]
public float DragY { get; init; }
[IgnoreMember]
public Vector2 DragPosition
{
get => new Vector2(DragX, DragY);
init
{
DragX = value.X;
DragY = value.Y;
}
}
}
}
+1 -1
View File
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Mods
player.DimmableStoryboard.IgnoreUserSettings.Value = true;
player.BreakOverlay.Hide();
player.OverlayComponents.Hide();
(player as ReplayPlayer)?.ReplayOverlay.Hide();
}
public bool PerformFail() => false;
@@ -41,6 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
protected override Container<Drawable> Content { get; }
private readonly Bindable<bool> trackRunning = new BindableBool();
private readonly Container overlayLayer;
private bool shouldBePlaying => Enabled.Value && IsHovered;
@@ -114,7 +115,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
overlayLayer.Add(new RippleVisualization(cardColours.Border)
{
TrackRunning = trackRunning.GetBoundCopy(),
TrackRunning = { BindTarget = trackRunning }
});
if (IsHovered)
@@ -124,12 +125,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
protected override bool OnHover(HoverEvent e)
{
if (previewTrack != null)
previewTrack.Looping = true;
if (shouldBePlaying)
{
startPreviewIfAvailable();
}
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (previewTrack != null)
previewTrack.Looping = false;
base.OnHoverLost(e);
}
private void onTrackStarted() => Schedule(() => trackRunning.Value = true);
private void onTrackStopped() => Schedule(() => trackRunning.Value = false);
@@ -193,7 +207,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
[Resolved]
private SongPreviewParticleContainer? particleContainer { get; set; }
public required IBindable<bool> TrackRunning { get; init; }
public readonly IBindable<bool> TrackRunning = new Bindable<bool>();
private readonly Color4 accentColour;
private readonly Container rippleContainer;
@@ -254,6 +268,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card
this.FadeOut(200);
}
}, true);
FinishTransforms();
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
@@ -0,0 +1,111 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Overlays;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
{
public partial class BackgroundMusicManager : CompositeComponent
{
private const int hover_fade_duration = 250;
private ScheduledDelegate? globalTrackFadeDelegate;
private DrawableTrack bgm = null!;
private bool shouldBePlaying;
private Bindable<bool> isPlayingPreview = null!;
[Resolved]
private MusicController musicController { get; set; } = null!;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
AddInternal(bgm = new DrawableTrack(audio.Tracks.Get("rankedplay_bgm.ogg")));
}
protected override void LoadComplete()
{
base.LoadComplete();
isPlayingPreview = previewTrackManager.IsPlayingPreview.GetBoundCopy();
isPlayingPreview.BindValueChanged(playing =>
{
bgm.VolumeTo(playing.NewValue ? 0 : 1, hover_fade_duration);
});
}
public void Play() => shouldBePlaying = true;
public void Stop() => shouldBePlaying = false;
protected override void Update()
{
base.Update();
updatePlayingState();
}
private void updatePlayingState()
{
if (!bgm.IsLoaded)
return;
if (shouldBePlaying == bgm.IsRunning)
return;
if (shouldBePlaying)
{
const int track_fade_duration = 3000;
// remove music control from player, to prevent overlapping music
musicController.AllowTrackControl.Value = false;
globalTrackFadeDelegate?.Cancel();
// cross-fade if global track is playing something
if (musicController.IsPlaying)
{
var globalTrack = musicController.CurrentTrack;
globalTrack.VolumeTo(0, track_fade_duration, Easing.OutCubic);
globalTrackFadeDelegate = Scheduler.AddDelayed(() =>
{
musicController.Stop();
globalTrack.VolumeTo(1);
}, track_fade_duration);
}
bgm.VolumeTo(0)
.VolumeTo(1, track_fade_duration, Easing.InCubic);
bgm.Looping = true;
bgm.Start();
}
else
{
globalTrackFadeDelegate?.Cancel();
bgm.Stop();
bgm.Reset();
// return control of music to player and reset volume
musicController.AllowTrackControl.Value = true;
musicController.CurrentTrack.Volume.Value = 1;
musicController.EnsurePlayingSomething();
}
}
}
}
@@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@@ -13,11 +13,13 @@ using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Users.Drawables;
using osuTK;
using osuTK.Graphics;
@@ -33,21 +35,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
Value = 1_000_000,
};
[Resolved]
private UserLookupCache users { get; set; } = null!;
private readonly int userId;
private readonly APIUser user;
private readonly Anchor contentAnchor;
private readonly RankedPlayColourScheme colourScheme;
private BufferedContainer grayScaleContainer = null!;
private OsuSpriteText beatmapState = null!;
private BeatmapAvailability availability = BeatmapAvailability.Unknown();
[Resolved]
private MultiplayerClient client { get; set; } = null!;
[Resolved]
private RankedPlayCornerPiece? cornerPiece { get; set; }
public RankedPlayUserDisplay(int userId, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
public RankedPlayUserDisplay(APIUser user, Anchor contentAnchor, RankedPlayColourScheme colourScheme)
{
this.userId = userId;
this.user = user;
this.contentAnchor = contentAnchor;
this.colourScheme = colourScheme;
}
@@ -55,12 +61,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
[BackgroundDependencyLoader]
private void load()
{
APIUser user = users.GetUserAsync(userId).GetResultSafely()!;
var shear = contentAnchor == Anchor.TopLeft || contentAnchor == Anchor.BottomRight
? -OsuGame.SHEAR
: OsuGame.SHEAR;
var beatmapStateAnchor = (contentAnchor & Anchor.x0) != 0
? Anchor.CentreLeft
: Anchor.CentreRight;
InternalChildren =
[
new CircularContainer
@@ -103,15 +111,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
Anchor = contentAnchor,
Origin = contentAnchor,
},
new OsuSpriteText
new FillFlowContainer
{
Name = "Username",
Text = user.Username,
Name = "Username/beatmap state container",
AutoSizeAxes = Axes.Both,
Anchor = contentAnchor,
Origin = contentAnchor,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Horizontal = 4, Vertical = 6 },
Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold),
UseFullGlyphHeight = false,
Spacing = new Vector2(5, 0),
Children =
[
new OsuSpriteText
{
Name = "Username",
Text = user.Username,
Anchor = contentAnchor,
Origin = contentAnchor,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.SemiBold),
UseFullGlyphHeight = false,
},
beatmapState = new OsuSpriteText
{
Anchor = beatmapStateAnchor,
Origin = beatmapStateAnchor,
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
},
],
},
]
}
@@ -129,6 +155,46 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Components
grayScaleContainer.GrayscaleTo(e.NewValue <= 0 ? 1 : 0, 300);
cornerPiece?.OnHealthChanged(e.NewValue);
});
client.RoomUpdated += onRoomUpdated;
}
private void onRoomUpdated()
{
var multiplayerUser = client.Room?.Users.SingleOrDefault(u => u.UserID == user.Id);
if (multiplayerUser == null || availability == multiplayerUser.BeatmapAvailability)
return;
availability = multiplayerUser.BeatmapAvailability;
if (availability.State is DownloadState.NotDownloaded or DownloadState.Downloading or DownloadState.Importing)
beatmapState.FadeIn(50);
else
beatmapState.FadeOut(50);
switch (availability.State)
{
case DownloadState.NotDownloaded:
beatmapState.Text = "Missing Beatmap";
break;
case DownloadState.Downloading:
double progress = Math.Clamp(availability.DownloadProgress ?? 0, 0, 1);
beatmapState.Text = $"Downloading... ({progress:P0})";
break;
case DownloadState.Importing:
beatmapState.Text = "Importing...";
break;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
client.RoomUpdated -= onRoomUpdated;
}
public partial class HealthBar : CompositeDrawable
@@ -34,7 +34,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Discard Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Discard Phase";
protected override LocalisableString StageCaption => "Replace cards from your hand";
private PlayerHandOfCards playerHand = null!;
@@ -81,6 +82,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
];
CenterColumn.Children =
[
discardButton = new ShearedButton
{
Name = "Discard Button",
@@ -89,11 +94,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Width = 150,
Action = onDiscardButtonClicked,
Enabled = { Value = true },
}
];
CenterColumn.Children =
[
},
playerHand = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
@@ -179,23 +180,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
base.OnEntering(previous);
var screenBottomCenter = new Vector2(DrawWidth / 2, DrawHeight);
int cardCount = 0;
double delay = 0;
const double stagger = 50;
foreach (var card in matchInfo.PlayerCards)
{
double currentDelay = delay;
playerHand.AddCard(card, c =>
{
c.Position = ToSpaceOfOtherDrawable(screenBottomCenter, playerHand);
c.Position = playerHand.BottomCardInsertPosition;
c.DelayMovementOnEntering(currentDelay);
});
Scheduler.AddDelayed(() =>
{
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
}, 50 * cardCount);
cardCount++;
}
playerHand.UpdateLayout(stagger: 50);
Scheduler.AddDelayed(() => SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample), delay);
delay += stagger;
}
}
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
@@ -297,8 +298,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
playerHand.AddCard(card, d =>
{
d.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth, DrawHeight * 0.5f), playerHand);
d.Rotation = -30;
// card should enter from centre-right of screen
var cardEnterPosition = ToSpaceOfOtherDrawable(new Vector2(DrawWidth, DrawHeight * 0.5f), playerHand);
d.SetupMovementForDrawnCard(cardEnterPosition);
});
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
@@ -4,6 +4,8 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -25,7 +27,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
/// </summary>
public Action<bool>? ExitRequested { get; init; }
protected override LocalisableString StageHeading => "Results";
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
[Resolved]
@@ -36,8 +38,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private OsuTextFlowContainer localRatingText = null!;
private OsuTextFlowContainer opponentRatingText = null!;
private Sample winSample = null!;
private Sample loseSample = null!;
private Sample drawSample = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load(OsuColour colours, AudioManager audio)
{
CenterColumn.Child = new FillFlowContainer
{
@@ -172,6 +178,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
}
};
winSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/win");
loseSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/lose");
drawSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/draw");
RankedPlayUserInfo localUser = matchInfo.RoomState.Users[Client.LocalUser!.UserID];
RankedPlayUserInfo otherUser = matchInfo.RoomState.Users.Values.Single(u => u != localUser);
@@ -179,16 +189,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
titleText.Text = "DRAW";
titleText.Colour = titleSeparator.Colour = colours.Orange1;
drawSample.Play();
}
else if (matchInfo.RoomState.WinningUserId == Client.LocalUser!.UserID)
{
titleText.Text = "VICTORY";
titleText.Colour = titleSeparator.Colour = colours.Green1;
winSample.Play();
}
else
{
titleText.Text = "DEFEAT";
titleText.Colour = titleSeparator.Colour = colours.Red1;
loseSample.Play();
}
localRatingText.AddText("Your Rating: ", s => s.Font = OsuFont.Style.Heading1.With(weight: FontWeight.Regular));
@@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class GameplayScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Gameplay";
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[BackgroundDependencyLoader]
@@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public override bool ShowBeatmapBackground => true;
protected override LocalisableString StageHeading => "Gameplay";
public override LocalisableString StageHeading => "Gameplay";
protected override LocalisableString StageCaption => string.Empty;
[Cached(typeof(IBindable<SongSelect.BeatmapSetLookupResult?>))]
@@ -1,11 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Game.Online.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
using osuTK;
@@ -16,8 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
{
public partial class HandCard : CompositeDrawable
{
public float LayoutWidth => DrawWidth * (State.Hovered ? hover_scale : 1);
private readonly Bindable<RankedPlayCardState> state = new Bindable<RankedPlayCardState>();
public RankedPlayCardState State
@@ -44,6 +43,28 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
set => State = State with { Pressed = value };
}
public bool CardDragged
{
get => State.Dragged;
set => State = State with { Dragged = value };
}
public bool CardHoveredOrDragged => CardHovered || CardDragged;
public Vector2 DragPosition
{
get => State.DragPosition;
set => State = State with { DragPosition = value };
}
public int Order
{
get => State.Order;
set => State = State with { Order = value };
}
public CardLayout LayoutTarget { get; set; }
[Resolved]
private HandOfCards handOfCards { get; set; } = null!;
@@ -63,20 +84,24 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
AddInternal(Card = card);
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
}
protected override void LoadComplete()
{
base.LoadComplete();
positionSpring.Current = positionSpring.PreviousTarget = Position;
scaleSpring.Current = scaleSpring.PreviousTarget = 1;
rotationSpring.Current = rotationSpring.PreviousTarget = Rotation;
state.BindValueChanged(OnStateChanged, true);
}
protected virtual void OnStateChanged(ValueChangedEvent<RankedPlayCardState> state)
{
handOfCards.OnCardStateChanged(this, state.NewValue);
handOfCards.OnCardStateChanged(this, state);
Card.ShowSelectionOutline = state.NewValue.Selected;
@@ -90,6 +115,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
Card.ScaleTo(1f, 400, Easing.OutElasticHalf);
break;
}
if (state.NewValue.Dragged)
{
// while card is being dragged card should slowly swing from side to side,
// so frequency is lowered and elasticity is increased
rotationSpring.NaturalFrequency = 2f;
rotationSpring.Damping = 0.4f;
rotationSpring.Response = 1.2f;
}
else
{
// otherwise rotation should be more snappy and not feel elastic
rotationSpring.NaturalFrequency = 3f;
rotationSpring.Damping = 0.75f;
rotationSpring.Response = 0.8f;
}
}
public RankedPlayCard Detach()
@@ -102,11 +143,92 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
return Card;
}
private bool updateMovement = true;
private static readonly SpringParameters default_position_spring_parameters = new SpringParameters
{
NaturalFrequency = 4f,
Response = 1.1f,
Damping = 0.8f
};
private readonly Vector2Spring positionSpring = new Vector2Spring { Parameters = default_position_spring_parameters };
private readonly FloatSpring rotationSpring = new FloatSpring
{
NaturalFrequency = 2f,
Damping = 0.4f,
Response = 1.2f,
};
private readonly FloatSpring scaleSpring = new FloatSpring
{
NaturalFrequency = 4f,
Response = 1.3f,
Damping = 0.75f,
Current = 1,
PreviousTarget = 1,
};
protected override void Update()
{
base.Update();
Card.Elevation = float.Lerp(CardHovered ? 1 : 0, Card.Elevation, (float)Math.Exp(-0.03f * Time.Elapsed));
if (updateMovement)
{
Position = positionSpring.Update(Time.Elapsed, LayoutTarget.Position);
Scale = new Vector2(scaleSpring.Update(Time.Elapsed, LayoutTarget.Scale));
float targetRotation = LayoutTarget.Rotation;
if (CardDragged)
{
targetRotation += positionSpring.Velocity.X * 0.006f;
}
Rotation = rotationSpring.Update(Time.Elapsed, targetRotation);
Card.Elevation = (float)Interpolation.DampContinuously(Card.Elevation, CardHoveredOrDragged ? 1 : 0, 25, Time.Elapsed);
}
}
/// <summary>
/// Delays the time until a card starts to move to its layout position, intended to use for staggered movement when adding multiple cards to the hand at once.
/// Movement is slowed down a bit while it's moving towards the target position to make the transition appear less abrupt.
/// </summary>
public void DelayMovementOnEntering(double delay)
{
const double approximate_time_until_position_reached = 200;
updateMovement = false;
this.Delay(delay)
.Schedule(() =>
{
updateMovement = true;
positionSpring.NaturalFrequency = 2.5f;
})
.Delay(approximate_time_until_position_reached)
.Schedule(() =>
{
positionSpring.Parameters = default_position_spring_parameters;
});
}
/// <summary>
/// Makes the card move towards its layout position from a given <paramref name="position"/> and updates
/// movement parameters so the card moves towards it's target position more slowly and less springy.
/// </summary>
public void SetupMovementForDrawnCard(Vector2 position)
{
const double approximate_time_until_position_reached = 200;
Position = position;
positionSpring.NaturalFrequency = 2f;
positionSpring.Damping = 1f;
Scheduler.AddDelayed(() => positionSpring.Parameters = default_position_spring_parameters, approximate_time_until_position_reached);
}
}
}
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -23,15 +24,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
[Cached]
public abstract partial class HandOfCards : CompositeDrawable
{
private const float hover_scale = 1.2f;
protected const float HOVER_SCALE = 1.2f;
public IEnumerable<HandCard> Cards => cardContainer.Children;
private const float card_spacing = -15;
public IReadOnlyList<HandCard> Cards => cardContainer.Children;
/// <summary>
/// How far a card slides upwards when hovered.
/// Used for making sure a card moves entirely into frame when the hand is partially off-screen.
/// </summary>
public float HoverYOffset = 15;
public float HoverYOffset = 35;
/// <summary>
/// If true, card layout will be flipped on both axes for a card hand placed at the top edge of the screen, while keeping the cards upright.
@@ -39,13 +42,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
/// </summary>
protected virtual bool Flipped => false;
private readonly Container<HandCard> cardContainer;
/// <summary>
/// Position to insert cards at so they are start moving from the bottom relative to the card layout
/// </summary>
public Vector2 BottomCardInsertPosition => new Vector2(0, (DrawHeight + RankedPlayCard.SIZE.Y) / 2 * (Flipped ? -1 : 1));
private readonly CardContainer cardContainer;
private readonly Dictionary<RankedPlayCardItem, HandCard> cardLookup = new Dictionary<RankedPlayCardItem, HandCard>();
protected HandOfCards()
{
AddInternal(cardContainer = new Container<HandCard>
AddInternal(cardContainer = new CardContainer
{
RelativeSizeAxes = Axes.Both,
});
@@ -55,6 +63,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
{
base.Update();
if (!drawOrderBacking.IsValid)
{
cardContainer.Sort();
drawOrderBacking.Validate();
}
if (!layoutBacking.IsValid)
{
updateLayout();
@@ -76,17 +90,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
foreach (var card in cardContainer)
{
card.Delay(delay)
.MoveTo(new Vector2(0, Flipped ? -220 : 220), 400, Easing.OutExpo)
.RotateTo(0, 400, Easing.OutExpo)
.ScaleTo(1, 400, Easing.OutExpo);
Scheduler.AddDelayed(() =>
{
card.LayoutTarget = new CardLayout
{
Position = new Vector2(0, (DrawHeight + RankedPlayCard.SIZE.Y + 10) / 2 * (Flipped ? -1 : 1)),
Rotation = 0,
Scale = 1,
};
}, delay);
delay += 50;
}
}
private Anchor cardAnchor => Flipped ? Anchor.TopCentre : Anchor.BottomCentre;
public void AddCard(RankedPlayCardWithPlaylistItem item, Action<HandCard>? setupAction = null) => AddCard(new RankedPlayCard(item), setupAction);
public void AddCard(RankedPlayCard card, Action<HandCard>? setupAction = null)
@@ -95,12 +112,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
return;
var drawable = CreateHandCard(card);
drawable.Anchor = drawable.Origin = cardAnchor;
cardLookup[card.Item.Card] = drawable;
drawable.Position = GetArcPosition(0);
if (card.Item.DisplayOrder != null)
drawable.Order = card.Item.DisplayOrder.Value;
else if (cardContainer.Count > 0)
drawable.Order = cardContainer.Max(c => c.Order) + 1;
cardContainer.Add(drawable);
layoutBacking.Invalidate();
InvalidateLayout(drawOrder: true);
setupAction?.Invoke(drawable);
}
@@ -113,8 +136,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
return false;
cardContainer.Remove(drawable, true);
layoutBacking.Invalidate();
return false;
InvalidateLayout(drawOrder: true);
return true;
}
/// <summary>
@@ -137,19 +160,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
card = drawable.Detach();
cardContainer.Remove(drawable, true);
layoutBacking.Invalidate();
InvalidateLayout(drawOrder: true);
return true;
}
protected virtual HandCard CreateHandCard(RankedPlayCard card) => new HandCard(card);
protected virtual void OnCardStateChanged(HandCard card, RankedPlayCardState state)
protected virtual void OnCardStateChanged(HandCard card, ValueChangedEvent<RankedPlayCardState> evt)
{
InvalidateLayout();
InvalidateLayout(drawOrder: affectsDrawOrder(evt));
// hovered state can be caused by keyboard focus, in which case we have to clean up after the other cards manually
if (state.Hovered)
if (evt.NewValue.Hovered)
{
foreach (var c in cardContainer)
{
@@ -159,95 +182,195 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
}
}
private static bool affectsDrawOrder(ValueChangedEvent<RankedPlayCardState> evt) =>
evt.OldValue.Order != evt.NewValue.Order ||
evt.OldValue.Dragged != evt.NewValue.Dragged;
#region Layout
private readonly Cached layoutBacking = new Cached();
private readonly Cached drawOrderBacking = new Cached();
protected void InvalidateLayout() => layoutBacking.Invalidate();
public void UpdateLayout(double stagger = 0)
/// <summary>
/// Invalidates the layout of the hand of cards, causing a relayout to occur.
/// </summary>
/// <param name="drawOrder">If set to true, also invalidates the draw order of the cards.</param>
protected void InvalidateLayout(bool drawOrder = false)
{
updateLayout(stagger);
layoutBacking.Validate();
layoutBacking.Invalidate();
if (drawOrder)
drawOrderBacking.Invalidate();
}
private void updateLayout(double stagger = 0)
private void updateLayout()
{
if (Contracted)
return;
const float spacing = -20;
// card container draws dragged card on top so we need to sort those separately
var cards = cardContainer.Children.OrderBy(static c => c.State.Order).ToArray();
float totalWidth = cardContainer.Sum(it => it.LayoutWidth + spacing) - spacing;
int activeCardIndex = GetActiveCardIndex(cards);
float x = -totalWidth / 2;
const int no_card_hovered = -1;
int hoverIndex = no_card_hovered;
for (int i = 0; i < cardContainer.Count; i++)
for (int i = 0; i < cards.Length; i++)
{
if (cardContainer[i].CardHovered)
{
hoverIndex = i;
break;
}
var card = cards[i];
var layout = card.CardDragged
? CalculateDraggedCardLayout(card.DragPosition)
: CalculateCardLayout(i, activeCardIndex);
if (Flipped)
layout.Position *= -1;
card.LayoutTarget = layout;
}
}
protected int GetActiveCardIndex(IReadOnlyList<HandCard> cards)
{
// the mouse can temporarily leave the dragged card, so dragged card should take precedence
for (int i = 0; i < cards.Count; i++)
{
if (cards[i].CardDragged)
return i;
}
double delay = 0;
for (int i = 0; i < cardContainer.Count; i++)
for (int i = 0; i < cards.Count; i++)
{
var child = cardContainer[i];
if (cards[i].CardHovered)
return i;
}
x += child.LayoutWidth / 2;
return -1;
}
float yOffset = 0;
protected CardLayout CalculateCardLayout(int index, int activeIndex)
{
float x = GetCardX(index, activeIndex);
var position = new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 - 10);
var position = GetArcPosition(x);
float rotation = GetArcRotation(x);
if (hoverIndex != no_card_hovered && cardContainer.Children.Count > 1)
{
int distance = Math.Abs(i - hoverIndex);
int direction = Math.Sign(i - hoverIndex);
if (index == activeIndex)
position += GetCardUpwardsDirection(rotation) * HoverYOffset;
position.X += direction switch
return new CardLayout
{
Position = position,
Rotation = rotation,
Scale = index == activeIndex ? HOVER_SCALE : 1,
};
}
protected virtual CardLayout CalculateDraggedCardLayout(Vector2 dragPosition)
{
return new CardLayout
{
Position = dragPosition,
Rotation = 0,
Scale = HOVER_SCALE,
};
}
/// <summary>
/// Represents the total width of the layout for all cards in the hand.
/// </summary>
/// <remarks>
/// Does not account for extra space needed for spreading the cards adjacent to the active card apart.
/// </remarks>
protected float TotalLayoutWidth => cardContainer.Count * (RankedPlayCard.SIZE.X + card_spacing) - card_spacing;
protected float GetCardX(int index, int activeIndex)
{
float x = -TotalLayoutWidth / 2
+ index * (RankedPlayCard.SIZE.X + card_spacing)
+ RankedPlayCard.SIZE.X / 2;
if (activeIndex < 0 || cardContainer.Count <= 1)
return x;
// if a card is hovered or dragged, the adjacent cards should get spread apart
int distance = Math.Abs(index - activeIndex);
int direction = Math.Sign(index - activeIndex);
float baseOffset = RankedPlayCard.SIZE.X * 0.1f;
switch (direction)
{
case -1:
if (cardContainer.Count == 2)
{
0 => 0,
// special case for the left card when there's only 2 cards
// too much offset looks kinda odd here so it's reduced
< 0 when cardContainer.Count == 2 => -3,
x -= baseOffset + 3;
break;
}
< 0 => -10 / MathF.Pow(distance, 3),
x -= baseOffset + 10 / MathF.Pow(distance, 2);
break;
// cards right to the hovered card have a higher offset because they are partially
// covering the cards to their left
> 0 => 20 / MathF.Pow(distance, 2),
};
case 1:
// cards right to the active card have a higher offset because they are partially
// covering the cards to their left
x += baseOffset + 20 / MathF.Pow(distance, 2);
break;
}
return x;
}
/// <summary>
/// Calculates the position of a card at a given <paramref name="x" /> coordinate so all cards are laid out in an arc
/// </summary>
protected Vector2 GetArcPosition(float x)
{
float offset = (DrawHeight - RankedPlayCard.SIZE.Y) / 2;
return new Vector2(x, MathF.Pow(MathF.Abs(x / 250), 2) * 20 + offset);
}
/// <summary>
/// Calculates the rotation of a card at a given <paramref name="x"/> coordinate
/// </summary>
protected static float GetArcRotation(float x) => x * 0.03f;
protected static Vector2 GetCardUpwardsDirection(float rotation)
{
float angle = MathHelper.DegreesToRadians(rotation - 90);
return new Vector2(MathF.Cos(angle), MathF.Sin(angle));
}
private partial class CardContainer : Container<HandCard>
{
protected override int Compare(Drawable x, Drawable y)
{
if (x is HandCard c1 && y is HandCard c2)
{
// dragged cards should always be drawn on top
if (c1.CardDragged)
return 1;
if (c2.CardDragged)
return -1;
int result = c1.Order.CompareTo(c2.Order);
if (result != 0)
return result;
}
if (child.CardHovered)
yOffset = -HoverYOffset;
float rotation = x * 0.03f;
float angle = MathHelper.DegreesToRadians(rotation + 90);
position += new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * yOffset;
position *= Flipped ? -1 : 1;
child
.Delay(delay)
.MoveTo(position, 300, Easing.OutExpo)
.RotateTo(rotation, 300, Easing.OutExpo)
.ScaleTo(child.CardHovered ? hover_scale : 1f, 400, Easing.OutElasticQuarter);
x += child.LayoutWidth / 2 + spacing;
delay += stagger;
return base.Compare(x, y);
}
public void Sort() => SortInternal();
}
public struct CardLayout
{
public required Vector2 Position { get; set; }
public required float Rotation { get; set; }
public required float Scale { get; set; }
}
#endregion
@@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
/// <summary>
/// Maximum amount of frames that can get queued up at the same time
/// </summary>
public int MaxQueuedFrames { get; set; } = 20;
public int MaxQueuedFrames { get; set; } = 30;
private readonly int userId;
private readonly OpponentHandOfCards handOfCards;
@@ -20,7 +20,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
/// <summary>
/// Minimum interval between individual replay frames
/// </summary>
public double RecordInterval { get; init; } = 25;
public double RecordInterval { get; init; } = 50;
/// <summary>
/// Max amount of frames to collect per <see cref="FlushInterval"/>
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using osu.Game.Online.RankedPlay;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
{
@@ -24,5 +25,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
card.State = cardState;
}
}
protected override CardLayout CalculateDraggedCardLayout(Vector2 dragPosition)
{
// the opponent shouldn't be able to drag his card across the entire screen.
// card movement is limited to roughly the width of the hand horizontally
// and has a fixed vertical offset (extended slightly further than when hovered)
float maxExtent = TotalLayoutWidth / 2;
float x = float.Clamp(dragPosition.X, -maxExtent, maxExtent);
return new CardLayout
{
Position = GetArcPosition(x) + new Vector2(0, -60),
Rotation = 0,
Scale = HOVER_SCALE,
};
}
}
}
@@ -33,6 +33,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
public required Action<PlayerHandCard> Clicked;
public required Action<PlayerHandCard, Vector2> Dragged;
public required IBindable<bool> AllowSelection;
private readonly Drawable cardInputArea;
@@ -165,6 +167,35 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
CardHovered = false;
}
#region Drag/Drop
private Vector2 dragOffset;
protected override bool OnDragStart(DragStartEvent e)
{
dragOffset = DrawPosition + AnchorPosition - e.MouseDownPosition;
CardDragged = true;
return true;
}
protected override void OnDrag(DragEvent e)
{
DragPosition = e.MousePosition - AnchorPosition + dragOffset;
Dragged(this, e.ScreenSpaceMousePosition);
}
protected override void OnDragEnd(DragEndEvent e)
{
base.OnDragEnd(e);
CardDragged = false;
}
#endregion
}
}
}
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@@ -12,6 +13,7 @@ using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Online.RankedPlay;
using osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Card;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
@@ -103,6 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
protected override HandCard CreateHandCard(RankedPlayCard card) => new PlayerHandCard(card)
{
Clicked = cardClicked,
Dragged = cardDragged,
AllowSelection = allowSelection.GetBoundCopy(),
PlayAction = PlayCardAction,
};
@@ -138,18 +141,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
}
}
protected override void OnCardStateChanged(HandCard card, RankedPlayCardState state)
protected override void OnCardStateChanged(HandCard card, ValueChangedEvent<RankedPlayCardState> evt)
{
StateChanged?.Invoke();
base.OnCardStateChanged(card, state);
base.OnCardStateChanged(card, evt);
}
public Dictionary<Guid, RankedPlayCardState> State => Cards.Select(static card => new KeyValuePair<Guid, RankedPlayCardState>(card.Item.Card.ID, card.State)).ToDictionary();
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || Contracted)
if (e.Repeat || Contracted || Cards.Any(static c => c.CardDragged))
return false;
switch (e.Key)
@@ -196,8 +199,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
int newIndex = currentIndex + direction;
if (newIndex < 0)
newIndex = Cards.Count() - 1;
else if (newIndex >= Cards.Count())
newIndex = Cards.Count - 1;
else if (newIndex >= Cards.Count)
newIndex = 0;
focusCard(newIndex);
@@ -215,5 +218,55 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Hand
if (SelectionMode == HandSelectionMode.Single && !card.Selected)
card.TriggerClick();
}
private void cardDragged(PlayerHandCard card, Vector2 screenSpacePosition)
{
var cards = Cards.OrderBy(static c => c.Order).ToArray();
int newIndex = cardIndexInLayout(cards, card.ScreenSpaceDrawQuad.Centre);
card.Order = newIndex;
int order = 0;
foreach (var c in cards)
{
if (order == newIndex)
order++;
if (c == card)
continue;
c.Order = order++;
}
foreach (var c in Cards)
c.Item.DisplayOrder = c.Order;
}
private int cardIndexInLayout(HandCard[] cards, Vector2 screenSpacePosition)
{
Debug.Assert(cards.Length > 0);
var position = ToLocalSpace(screenSpacePosition) - DrawSize / 2;
int activeIndex = GetActiveCardIndex(cards);
int minIndex = 0;
float minDistance = float.MaxValue;
for (int i = 0; i < cards.Length; i++)
{
float distance = MathF.Abs(GetCardX(i, activeIndex) - position.X);
if (distance < minDistance)
{
minDistance = distance;
minIndex = i;
}
}
return minIndex;
}
}
}
@@ -1,7 +1,6 @@
// 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.Tasks;
using osu.Framework.Allocation;
@@ -15,13 +14,12 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Overlays;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
{
public partial class IntroScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => string.Empty;
public override LocalisableString StageHeading => string.Empty;
protected override LocalisableString StageCaption => string.Empty;
public IntroScreen()
@@ -36,9 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private MusicController? musicController { get; set; }
private Sample? windupSample;
private Sample? impactSample;
private Sample? swooshSample;
@@ -81,8 +76,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
private StarRatingSequence? starRatingAnimation;
private IDisposable? duckOperation;
public void PlayIntroSequence(UserWithRating player, UserWithRating opponent, double starRating)
{
double delay = 0;
@@ -95,8 +88,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
vsScreen.Play(ref delay, out double impactDelay);
duckOperation = musicController?.Duck();
if (windupSample != null)
{
Scheduler.AddDelayed(() => windupSample?.Play(), impactDelay - windupSample.Length);
@@ -113,16 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay.Intro
{
starRatingAnimation?.PopOut();
duckOperation?.Dispose();
this.Delay(500).FadeOut();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
duckOperation?.Dispose();
}
}
}
@@ -23,7 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Pick Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "Waiting for your opponent...";
protected override RankedPlayColourScheme ColourScheme => RankedPlayColourScheme.Red;
@@ -34,6 +35,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
[Resolved]
private RankedPlayMatchInfo matchInfo { get; set; } = null!;
private Sample? cardAddSample;
private const int card_play_samples = 2;
private Sample?[]? cardPlaySamples;
@@ -56,15 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
CenterColumn.Children =
[
playerHand = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Y = 100,
HoverYOffset = 90
},
opponentHand = new OpponentHandOfCards
{
Anchor = Anchor.TopCentre,
@@ -72,10 +66,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
},
playerHand = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
},
new HandReplayRecorder(playerHand),
new HandReplayPlayer(matchInfo.OpponentId, opponentHand),
];
cardAddSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/card-add-1");
cardPlaySamples = new Sample?[card_play_samples];
for (int i = 0; i < card_play_samples; i++)
cardPlaySamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Ranked/card-play-{1 + i}");
@@ -85,24 +88,51 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
base.OnEntering(previous);
foreach (var card in matchInfo.PlayerCards)
const double stagger = 50;
double delay = 0;
foreach (var item in matchInfo.PlayerCards)
{
playerHand.AddCard(card, c =>
double currentDelay = delay;
if ((previous as DiscardScreen)?.CenterRow.RemoveCard(item, out var card, out var drawQuad) == true)
{
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, DrawHeight), playerHand);
});
playerHand.AddCard(card, c =>
{
c.MatchScreenSpaceDrawQuad(drawQuad, playerHand);
c.DelayMovementOnEntering(currentDelay);
});
}
else
{
playerHand.AddCard(item, c =>
{
c.Position = playerHand.BottomCardInsertPosition;
c.DelayMovementOnEntering(currentDelay);
});
Scheduler.AddDelayed(() =>
{
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
}, delay);
}
delay += stagger;
}
delay = 0;
foreach (var card in matchInfo.OpponentCards)
{
double currentDelay = delay;
opponentHand.AddCard(card, c =>
{
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), playerHand);
c.Position = opponentHand.BottomCardInsertPosition;
c.DelayMovementOnEntering(currentDelay);
});
}
playerHand.UpdateLayout(stagger: 50);
opponentHand.UpdateLayout(stagger: 50);
delay += 50;
}
}
protected override void LoadComplete()
@@ -28,7 +28,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public CardFlow CenterRow { get; private set; } = null!;
protected override LocalisableString StageHeading => "Pick Phase";
public override bool ShowStageOverlay => true;
public override LocalisableString StageHeading => "Pick Phase";
protected override LocalisableString StageCaption => "It's your turn to play a card!";
private PlayerHandOfCards playerHand = null!;
@@ -73,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
CenterColumn.Children =
[
opponentHand = new OpponentHandOfCards
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Y = -100,
},
playerHand = new PlayerHandOfCards
{
Anchor = Anchor.BottomCentre,
@@ -82,14 +92,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
SelectionMode = HandSelectionMode.Single,
PlayCardAction = onPlayButtonClicked
},
opponentHand = new OpponentHandOfCards
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Y = -100,
},
new HandReplayRecorder(playerHand),
new HandReplayPlayer(matchInfo.OpponentId, opponentHand),
];
@@ -149,41 +151,51 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
base.OnEntering(previous);
int delay = 0;
const double stagger = 50;
double delay = 0;
foreach (var item in matchInfo.PlayerCards)
{
double currentDelay = delay;
if ((previous as DiscardScreen)?.CenterRow.RemoveCard(item, out var card, out var drawQuad) == true)
{
playerHand.AddCard(card, c =>
{
c.MatchScreenSpaceDrawQuad(drawQuad, playerHand);
c.DelayMovementOnEntering(currentDelay);
});
}
else
{
playerHand.AddCard(item, c =>
{
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, DrawHeight), playerHand);
c.Position = playerHand.BottomCardInsertPosition;
c.DelayMovementOnEntering(currentDelay);
});
Scheduler.AddDelayed(() =>
{
SamplePlaybackHelper.PlayWithRandomPitch(cardAddSample);
}, 50 * delay);
delay++;
}, delay);
}
delay += stagger;
}
delay = 0;
foreach (var item in matchInfo.OpponentCards)
{
double currentDelay = delay;
opponentHand.AddCard(item, c =>
{
c.Position = ToSpaceOfOtherDrawable(new Vector2(DrawWidth / 2, 0), playerHand);
c.DelayMovementOnEntering(currentDelay);
});
}
playerHand.UpdateLayout(stagger: 50);
opponentHand.UpdateLayout(stagger: 50);
delay += 50;
}
}
private void onCountdownStarted(MultiplayerCountdown countdown) => Scheduler.Add(() =>
@@ -13,6 +13,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public readonly Bindable<MultiplayerPlaylistItem?> PlaylistItem = new Bindable<MultiplayerPlaylistItem?>();
public readonly RankedPlayCardItem Card;
/// <summary>
/// The player's preferred display order for this card
/// </summary>
public int? DisplayOrder { get; set; }
public RankedPlayCardWithPlaylistItem(RankedPlayCardItem card)
{
Card = card;
@@ -74,6 +74,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public bool IsOwnTurn => RoomState.ActiveUserId == client.LocalUser?.UserID;
public bool IsOpponentTurn => RoomState.ActiveUserId == OpponentId;
public int CurrentRound => RoomState.CurrentRound;
public int OpponentId => RoomState.Users.Keys.Single(u => u != client.LocalUser?.UserID);
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -14,8 +15,10 @@ using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.RankedPlay;
using osu.Game.Online.Rooms;
@@ -56,6 +59,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private UserLookupCache users { get; set; } = null!;
[Resolved]
private IDialogOverlay dialogOverlay { get; set; } = null!;
@@ -69,6 +75,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private QueueController? controller { get; set; }
private readonly MultiplayerRoom room;
private APIUser localUser = null!;
private APIUser opponentUser = null!;
private readonly Container stageOverlayContainer;
private readonly Container<RankedPlaySubScreen> screenContainer;
private readonly RankedPlayChatDisplay chat;
@@ -88,6 +99,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
[Cached]
private readonly SongPreviewParticleContainer particleContainer;
[Cached]
private BackgroundMusicManager backgroundMusic;
public RankedPlayScreen(MultiplayerRoom room)
{
this.room = room;
@@ -127,8 +141,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
}
}
},
stageOverlayContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
overlayContainer = new CardDetailsOverlayContainer(),
particleContainer = new SongPreviewParticleContainer(),
backgroundMusic = new BackgroundMusicManager()
};
}
@@ -150,11 +169,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
int localUserId = api.LocalUser.Value.OnlineID;
int opponentUserId = ((RankedPlayRoomState)client.Room!.MatchState!).Users.Keys.Single(it => it != localUserId);
localUser = users.GetUserAsync(localUserId).GetResultSafely()!;
opponentUser = users.GetUserAsync(opponentUserId).GetResultSafely()!;
AddRangeInternal([
new RankedPlayCornerPiece(RankedPlayColourScheme.Blue, Anchor.BottomLeft)
{
State = { BindTarget = cornerPieceVisibility },
Child = new RankedPlayUserDisplay(localUserId, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = new RankedPlayUserDisplay(localUser, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
Health = { BindTarget = matchInfo.PlayerHealth }
@@ -163,7 +185,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
new RankedPlayCornerPiece(RankedPlayColourScheme.Red, Anchor.TopRight)
{
State = { BindTarget = cornerPieceVisibility },
Child = new RankedPlayUserDisplay(opponentUserId, Anchor.TopRight, RankedPlayColourScheme.Red)
Child = new RankedPlayUserDisplay(opponentUser, Anchor.TopRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
Health = { BindTarget = matchInfo.OpponentHealth }
@@ -195,6 +217,25 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
cornerPieceVisibility.BindTo(screen.CornerPieceVisibility);
showBeatmapBackground.Value = screen.ShowBeatmapBackground;
if (screen.ShowStageOverlay)
{
APIUser? pickingUser = null;
double? multiplier = matchInfo.Stage.Value < RankedPlayStage.CardPlay ? null : matchInfo.RoomState.DamageMultiplier;
RankedPlayColourScheme colourScheme = RankedPlayColourScheme.Blue;
if (matchInfo.Stage.Value == RankedPlayStage.CardPlay && matchInfo.RoomState.ActiveUser != null)
{
pickingUser = matchInfo.IsOwnTurn ? localUser : opponentUser;
colourScheme = matchInfo.IsOwnTurn ? RankedPlayColourScheme.Blue : RankedPlayColourScheme.Red;
}
stageOverlayContainer.Add(new RankedPlayStageOverlay(screen.StageHeading, colourScheme)
{
PickingUser = pickingUser,
Multiplier = multiplier,
});
}
};
}
@@ -224,6 +265,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
chat.Appear();
if (stage is RankedPlayStage.GameplayWarmup or RankedPlayStage.Gameplay)
backgroundMusic.Stop();
else
backgroundMusic.Play();
switch (stage)
{
case RankedPlayStage.RoundWarmup when matchInfo.CurrentRound == 1:
@@ -278,6 +324,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public override void OnSuspending(ScreenTransitionEvent e)
{
chat.Disappear();
backgroundMusic.Stop();
previewTrackManager.StopAnyPlaying(this);
base.OnSuspending(e);
@@ -296,6 +343,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
return true;
}
backgroundMusic.Stop();
previewTrackManager.StopAnyPlaying(this);
client.LeaveRoom().FireAndForget();
@@ -0,0 +1,186 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Users.Drawables;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class RankedPlayStageOverlay : CompositeDrawable
{
private readonly LocalisableString stageName;
private readonly RankedPlayColourScheme colourScheme;
public APIUser? PickingUser { get; init; }
public double? Multiplier { get; init; }
private FillFlowContainer displayContainer = null!;
private FillFlowContainer detailsContainer = null!;
private CircularContainer avatarContainer = null!;
public RankedPlayStageOverlay(LocalisableString stageName, RankedPlayColourScheme colourScheme)
{
this.stageName = stageName;
this.colourScheme = colourScheme;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new Box
{
Alpha = 0.4f,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = Color4Extensions.FromHex("#000"),
},
displayContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new Container
{
Width = 500,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = OsuGame.SHEAR,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourScheme.Surface.Darken(0.1f),
Alpha = 0.8f,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = -OsuGame.SHEAR,
Padding = new MarginPadding { Vertical = 20 },
Font = OsuFont.TorusAlternate.With(size: 72),
Shadow = false,
Text = stageName,
},
},
},
detailsContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(40, 0),
},
},
}
},
};
if (PickingUser != null)
{
detailsContainer.Add(new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
avatarContainer = new CircularContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(32),
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourScheme.Surface,
Alpha = 0.5f,
},
},
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 32),
Text = $"{PickingUser.Username}'s pick",
Colour = colourScheme.Primary,
},
},
});
}
if (Multiplier != null)
{
detailsContainer.Add(new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
UseFullGlyphHeight = false,
Font = OsuFont.Torus.With(size: 32),
Text = $"{Multiplier:N0}x damage",
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (PickingUser != null)
LoadComponentAsync(new DrawableAvatar(PickingUser), a => avatarContainer.Add(a));
const int duration = 500;
const int time_visible = 1500;
const Easing easing = Easing.OutQuint;
this.FadeInFromZero(300, easing);
displayContainer
.ScaleTo(0.9f)
.ScaleTo(1f, duration, easing);
using (BeginDelayedSequence(time_visible))
{
this.FadeOut(duration, easing)
.Expire();
displayContainer
.ScaleTo(0.9f, duration, easing);
}
}
}
}
@@ -23,10 +23,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
public virtual bool ShowBeatmapBackground => false;
/// <summary>
/// Whether a fullscreen overlay displaying the current stage (and any additional
/// information like the currently picking player and/or the damage multiplier)
/// should be displayed upon entering this screen.
/// </summary>
public virtual bool ShowStageOverlay => false;
/// <summary>
/// Heading text to be displayed indicating the purpose of the current stage.
/// </summary>
protected abstract LocalisableString StageHeading { get; }
public abstract LocalisableString StageHeading { get; }
/// <summary>
/// Subtitle text to be displayed indicating the action a user should take in the current stage.
@@ -6,6 +6,8 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
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;
@@ -37,7 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
public partial class ResultsScreen : RankedPlaySubScreen
{
protected override LocalisableString StageHeading => "Results";
public override LocalisableString StageHeading => "Results";
protected override LocalisableString StageCaption => string.Empty;
public override bool ShowBeatmapBackground => true;
@@ -205,8 +207,24 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private RankedPlayDamageInfo losingDamageInfo = null!;
private Sample resultsAppearSample = null!;
private Sample dmgFlySample = null!;
private Sample dmgHitSample = null!;
private Sample hpDownSample = null!;
private Sample playerAppearSample = null!;
private Sample pseudoScoreCounterSample = null!;
private Sample scoreTickSample = null!;
private Sample gradePassSample = null!;
private Sample gradePassSsSample = null!;
private Sample gradeFailSample = null!;
private Sample gradeFailDSample = null!;
private SampleChannel? playerScoreTickChannel;
private SampleChannel? opponentScoreTickChannel;
private readonly BindableDouble playerScoreTickPitch = new BindableDouble();
private readonly BindableDouble opponentScoreTickPitch = new BindableDouble();
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
// this works under the assumption that only one player can receive damage each round
losingDamageInfo = matchInfo.RoomState.Users
@@ -225,7 +243,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
State = { BindTarget = cornerPieceVisibility },
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.UserID, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
Child = playerUserDisplay = new RankedPlayUserDisplay(PlayerScore.User, Anchor.BottomLeft, RankedPlayColourScheme.Blue)
{
RelativeSizeAxes = Axes.Both,
Health = { Value = PlayerDamageInfo.OldLife }
@@ -236,7 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
State = { BindTarget = cornerPieceVisibility },
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.UserID, Anchor.BottomRight, RankedPlayColourScheme.Red)
Child = opponentUserDisplay = new RankedPlayUserDisplay(OpponentScore.User, Anchor.BottomRight, RankedPlayColourScheme.Red)
{
RelativeSizeAxes = Axes.Both,
Health = { Value = OpponentDamageInfo.OldLife }
@@ -417,6 +435,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
]
}
});
resultsAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/results-appear");
dmgFlySample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-fly");
dmgHitSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/dmg-hit");
hpDownSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/hp-down");
playerAppearSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/players-appear");
pseudoScoreCounterSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/pseudo-score-counter");
scoreTickSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Ranked/Results/score-tick");
gradePassSample = audio.Samples.Get(@"Results/rank-impact-pass");
gradePassSsSample = audio.Samples.Get(@"Results/rank-impact-pass-ss");
gradeFailSample = audio.Samples.Get(@"Results/rank-impact-fail");
gradeFailDSample = audio.Samples.Get(@"Results/rank-impact-fail-d");
}
protected override void LoadComplete()
@@ -436,6 +466,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
private void appear(ref double delay)
{
resultsAppearSample.Play();
panelScaffold.FadeIn(100)
.ResizeTo(0)
.ResizeTo(cardSize with { Y = 30 }, 600, Easing.OutExpo)
@@ -451,7 +483,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
playerScoreCounter.FadeIn(600);
opponentScoreCounter.FadeIn(600);
Schedule(() => cornerPieceVisibility.Value = Visibility.Visible);
Schedule(() =>
{
cornerPieceVisibility.Value = Visibility.Visible;
playerAppearSample.Play();
});
}
using (BeginDelayedSequence(900))
@@ -487,12 +523,57 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
playerScoreBar.FadeIn(100);
opponentScoreBar.FadeIn(100);
playerScoreTickChannel ??= scoreTickSample.GetChannel();
playerScoreTickChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH;
playerScoreTickChannel.Frequency.BindTarget = playerScoreTickPitch;
playerScoreTickPitch.Value = 0.5f;
playerScoreTickChannel.Looping = true;
opponentScoreTickChannel ??= scoreTickSample.GetChannel();
opponentScoreTickChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH;
opponentScoreTickChannel.Frequency.BindTarget = opponentScoreTickPitch;
opponentScoreTickPitch.Value = 0.5f;
opponentScoreTickChannel.Looping = true;
Schedule(() =>
{
if (losingDamageInfo.Damage > 0)
pseudoScoreCounterSample.Play();
if (PlayerScore.TotalScore > 0)
playerScoreTickChannel.Play();
if (OpponentScore.TotalScore > 0)
opponentScoreTickChannel.Play();
});
this.TransformBindableTo(scoreBarProgress, maxScorePercent, score_text_duration, new CubicBezierEasingFunction(easeIn: 0.4, easeOut: 1));
this.TransformBindableTo(playerScoreTickPitch, 0.5f + playerScorePercent, score_text_duration, Easing.OutCubic);
this.TransformBindableTo(opponentScoreTickPitch, 0.5f + opponentScorePercent, score_text_duration, Easing.OutCubic);
// safety timeout to ensure scoreTicks don't play forever
Scheduler.AddDelayed(() =>
{
if (playerScoreTickChannel != null)
playerScoreTickChannel.Looping = false;
if (opponentScoreTickChannel != null)
opponentScoreTickChannel.Looping = false;
}, score_text_duration + 500);
scoreBarProgress.BindValueChanged(e =>
{
playerScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, playerScorePercent));
opponentScoreBar.Height = float.Lerp(0.05f, 1f, Math.Min(e.NewValue, opponentScorePercent));
Schedule(() =>
{
if (playerScoreTickChannel != null && playerScoreBar.Height >= playerScorePercent)
playerScoreTickChannel.Looping = false;
if (opponentScoreTickChannel != null && opponentScoreBar.Height >= opponentScorePercent)
opponentScoreTickChannel.Looping = false;
});
});
}
@@ -503,6 +584,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
const double text_movement_duration = 400;
bool playerTookDamage = OpponentScore.TotalScore > PlayerScore.TotalScore;
double loserPanDirection = playerTookDamage ? -OsuGameBase.SFX_STEREO_STRENGTH : OsuGameBase.SFX_STEREO_STRENGTH;
using (BeginDelayedSequence(delay))
{
Schedule(() =>
@@ -522,6 +606,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
.ScaleTo(0.9f)
.ScaleTo(1f, 300, Easing.OutElasticHalf);
var dmgFlyChannel = dmgFlySample.GetChannel();
this.TransformBindableTo(dmgFlyChannel.Balance, loserPanDirection, text_movement_duration, Easing.InCubic);
dmgFlyChannel.Play();
flyingDamageText.FadeIn()
.MoveTo(position, text_movement_duration, Easing.InCubic)
.ScaleTo(0.75f, text_movement_duration, new CubicBezierEasingFunction(easeIn: 0.35, easeOut: 0.5))
@@ -531,6 +619,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
Scheduler.AddDelayed(() =>
{
var dmgHitChannel = dmgHitSample.GetChannel();
dmgHitChannel.Balance.Value = loserPanDirection;
dmgHitChannel.Play();
userDisplay.Shake(shakeDuration: 60, shakeMagnitude: 2, maximumLength: 120);
for (int i = 0; i < 10; i++)
@@ -564,6 +656,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
playerUserDisplay.Health.Value = PlayerDamageInfo.NewLife;
opponentUserDisplay.Health.Value = OpponentDamageInfo.NewLife;
Scheduler.AddDelayed(() =>
{
var hpDecreaseChannel = hpDownSample.GetChannel();
hpDecreaseChannel.Balance.Value = loserPanDirection;
hpDecreaseChannel.Play();
}, 900);
});
}
@@ -576,11 +675,45 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.RankedPlay
{
playerScoreDetails.FadeIn(300);
opponentScoreDetails.FadeIn(300);
Schedule(() =>
{
SampleChannel playerRankChannel = getRankSample(PlayerScore.Rank).GetChannel();
playerRankChannel.Balance.Value = -OsuGameBase.SFX_STEREO_STRENGTH;
playerRankChannel.Play();
SampleChannel opponentRankChannel = getRankSample(OpponentScore.Rank).GetChannel();
opponentRankChannel.Balance.Value = OsuGameBase.SFX_STEREO_STRENGTH;
opponentRankChannel.Play();
});
}
delay += 800;
}
private Sample getRankSample(ScoreRank rank)
{
switch (rank)
{
default:
case ScoreRank.D:
return gradeFailDSample;
case ScoreRank.C:
case ScoreRank.B:
return gradeFailSample;
case ScoreRank.A:
case ScoreRank.S:
case ScoreRank.SH:
return gradePassSample;
case ScoreRank.X:
case ScoreRank.XH:
return gradePassSsSample;
}
}
private static int numDigits(long value)
{
if (value <= 0)
@@ -32,6 +32,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private MatchStartCountdown? currentMatchStartCountdown => client.Room?.ActiveCountdowns.OfType<MatchStartCountdown>().SingleOrDefault();
private readonly MultiplayerReadyButton readyButton;
private readonly MultiplayerCountdownButton countdownButton;
@@ -111,22 +113,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
if (client.IsHost)
if (client.IsReferee)
{
if (client.Room.State == MultiplayerRoomState.Open && currentMatchStartCountdown == null)
startMatch();
else if (client.Room.State == MultiplayerRoomState.WaitingForLoad || client.Room.State == MultiplayerRoomState.Playing)
abortMatch();
}
else if (client.IsHost)
{
if (client.Room.State == MultiplayerRoomState.Open)
{
if (isReady() && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
if (isReady() && currentMatchStartCountdown == null)
startMatch();
else
toggleReady();
}
else
{
if (dialogOverlay == null)
abortMatch();
else
dialogOverlay.Push(new ConfirmAbortDialog(abortMatch, endOperation));
}
abortMatch();
}
else if (client.Room.State != MultiplayerRoomState.Closed)
toggleReady();
@@ -146,7 +150,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
endOperation();
});
void abortMatch() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
void performAbort() => client.AbortMatch().FireAndForget(endOperation, _ => endOperation());
void abortMatch()
{
if (dialogOverlay == null)
performAbort();
else
dialogOverlay.Push(new ConfirmAbortDialog(performAbort, endOperation));
}
}
private void startCountdown(TimeSpan duration)
@@ -159,14 +171,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void cancelCountdown()
{
if (client.Room == null)
if (client.Room == null || currentMatchStartCountdown == null)
return;
Debug.Assert(clickOperation == null);
clickOperation = ongoingOperationTracker.BeginOperation();
MultiplayerCountdown countdown = client.Room.ActiveCountdowns.Single(c => c is MatchStartCountdown);
client.SendMatchRequest(new StopCountdownRequest(countdown.ID)).ContinueWith(_ => endOperation());
client.SendMatchRequest(new StopCountdownRequest(currentMatchStartCountdown.ID)).ContinueWith(_ => endOperation());
}
private void endOperation()
@@ -186,10 +197,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
var localUser = client.LocalUser;
int newCountReady = client.Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = client.Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
int newCountReady = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready);
int newCountTotal = client.Room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating);
if (!client.IsHost || client.Room.Settings.AutoStartEnabled)
if ((!client.IsHost && !client.IsReferee) || client.Room.Settings.AutoStartEnabled || client.Room.State != MultiplayerRoomState.Open)
countdownButton.Hide();
else
{
@@ -214,12 +225,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
// When the local user is the host and spectating the match, the ready button should be enabled only if any users are ready.
if (localUser?.State == MultiplayerUserState.Spectating)
readyButton.Enabled.Value &= client.IsHost && newCountReady > 0 && !client.Room.ActiveCountdowns.Any(c => c is MatchStartCountdown);
readyButton.Enabled.Value &= (client.IsHost || client.IsReferee) && newCountReady > 0 && currentMatchStartCountdown == null;
// When the local user is not the host, the button should only be enabled when no match is in progress.
if (!client.IsHost)
// When the local user is not the host or a referee, the button should only be enabled when no match is in progress.
if (!client.IsHost && !client.IsReferee)
readyButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
// As a referee, readying up should not be possible, so if there is no match going on and no users readied up, prevent a match start.
if (client.IsReferee)
readyButton.Enabled.Value &= client.Room.State != MultiplayerRoomState.Open || newCountReady > 0;
// At all times, the countdown button should only be enabled when no match is in progress.
countdownButton.Enabled.Value &= client.Room.State == MultiplayerRoomState.Open;
@@ -120,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
});
}
if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && multiplayerClient.IsHost)
if (multiplayerClient.Room?.ActiveCountdowns.Any(c => c is MatchStartCountdown) == true && (multiplayerClient.IsHost || multiplayerClient.IsReferee))
{
flow.Add(new RoundedButton
{
@@ -122,14 +122,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
var localUser = multiplayerClient.LocalUser;
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
int countReady = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State == MultiplayerUserState.Ready);
int countTotal = room.Users.Count(u => u.Role == MultiplayerRoomUserRole.Player && u.State != MultiplayerUserState.Spectating);
string countText = $"({countReady} / {countTotal} ready)";
if (countdown != null)
{
string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
string? countdownText = countdown != null ? $"Starting in {countdownTimeRemaining:mm\\:ss}" : null;
if (multiplayerClient.IsReferee)
{
if (room.State == MultiplayerRoomState.Open)
Text = countReady == 0 ? $"Waiting for players... {countText}" : $"{countdownText ?? "Start match"} {countText}";
else
Text = "Abort match";
return;
}
if (countdownText != null)
{
switch (localUser?.State)
{
default:
@@ -196,7 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
default:
// Show the abort button for the host as long as gameplay is in progress.
if (multiplayerClient.IsHost && room.State != MultiplayerRoomState.Open)
if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && room.State != MultiplayerRoomState.Open)
setRed();
else
setGreen();
@@ -204,7 +214,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
if (multiplayerClient.IsHost && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
if ((multiplayerClient.IsHost || multiplayerClient.IsReferee) && !room.ActiveCountdowns.Any(c => c is MatchStartCountdown))
setGreen();
else
setYellow();
@@ -66,8 +66,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
if (multiplayerClient.Room == null)
return;
bool isItemOwner = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost;
bool isValidItem = isItemOwner && !Item.Expired;
bool isItemOwnerOrReferee = Item.OwnerID == api.LocalUser.Value.OnlineID || multiplayerClient.IsHost || multiplayerClient.IsReferee;
bool isValidItem = isItemOwnerOrReferee && !Item.Expired;
AllowDeletion = isValidItem
&& (Item.ID != multiplayerClient.Room.Settings.PlaylistItemId // This is an optimisation for the following check.
@@ -596,7 +596,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
break;
default:
targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users)));
if (!client.IsReferee)
targetScreen.Push(new MultiplayerPlayerLoader(() => new MultiplayerPlayer(room, new PlaylistItem(client.Room.CurrentPlaylistItem), users)));
break;
}
}
@@ -676,8 +677,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
Ruleset.Value = ruleset;
Mods.Value = client.LocalUser.Mods.Concat(item.RequiredMods).Select(m => m.ToMod(rulesetInstance)).ToArray();
bool freemods = item.Freestyle || item.AllowedMods.Any();
bool freestyle = item.Freestyle;
bool freemods = !client.IsReferee && (item.Freestyle || item.AllowedMods.Any());
bool freestyle = !client.IsReferee && item.Freestyle;
if (freemods)
userModsSection.Show();
@@ -921,8 +922,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room.CanAddPlaylistItems(client.LocalUser) != true)
return;
// If there's only one playlist item and we are the host, assume we want to change it. Else add a new one.
PlaylistItem? itemToEdit = client.IsHost && room.Playlist.Count == 1 ? room.Playlist.Single() : null;
// If there's only one playlist item and we are the host / a referee, assume we want to change it. Else add a new one.
PlaylistItem? itemToEdit = (client.IsHost || client.IsReferee) && room.Playlist.Count == 1 ? room.Playlist.Single() : null;
ShowSongSelect(itemToEdit);
@@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.Room == null || client.LocalUser == null)
return;
ChangeSettingsButton.Alpha = client.IsHost ? 1 : 0;
ChangeSettingsButton.Alpha = client.IsHost || client.IsReferee ? 1 : 0;
SelectedItem.Value = new PlaylistItem(client.Room.CurrentPlaylistItem);
});
@@ -295,7 +295,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStyleDisplay.FadeOut(fade_time);
}
kickButton.Alpha = client.IsHost && !user.Equals(client.LocalUser) ? 1 : 0;
kickButton.Alpha = (client.IsHost || client.IsReferee) && !user.Equals(client.LocalUser) ? 1 : 0;
crown.Alpha = client.Room.Host?.Equals(user) == true ? 1 : 0;
}
@@ -312,8 +312,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (user.UserID == api.LocalUser.Value.Id)
return null;
// If the local user is not the host of the room.
if (client.Room.Host?.UserID != api.LocalUser.Value.Id)
if (!client.IsHost && !client.IsReferee)
return null;
int targetUser = user.UserID;
@@ -322,8 +321,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
new OsuMenuItem("Give host", MenuItemType.Standard, () =>
{
// Ensure the local user is still host.
if (!client.IsHost)
// Ensure the local user is still host / a referee.
if (!client.IsHost && !client.IsReferee)
return;
client.TransferHost(targetUser).FireAndForget();
@@ -331,7 +330,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
new OsuMenuItem("Kick", MenuItemType.Destructive, () =>
{
// Ensure the local user is still host.
if (!client.IsHost)
if (!client.IsHost && !client.IsReferee)
return;
client.KickUser(targetUser).FireAndForget();
@@ -5,11 +5,16 @@ using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.Leaderboards;
using osu.Game.Screens.Ranking;
using FontWeight = osu.Game.Graphics.FontWeight;
using OsuFont = osu.Game.Graphics.OsuFont;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@@ -39,6 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
{
this.spectatorPlayerClock = spectatorPlayerClock;
ShowSettingsOverlay = false;
}
[BackgroundDependencyLoader]
@@ -54,8 +61,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
// also applied in `MultiplayerPlayer.load()`
ScoreProcessor.ApplyNewJudgementsWhenFailed = true;
HUDOverlay.PlayerSettingsOverlay.Expire();
HUDOverlay.HoldToQuit.Expire();
// Player username display
GameplayClockContainer.Add(new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Title.With(size: 60, weight: FontWeight.SemiBold))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Text = Score.ScoreInfo.User.Username,
Y = 50,
});
}
protected override void Update()
@@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private readonly Room room;
private PlayerSettingsOverlay playerSettingsOverlay = null!;
private ReplaySettingsOverlay replaySettingsOverlay = null!;
private Bindable<bool> configSettingsOverlay = null!;
/// <summary>
@@ -138,7 +138,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
ReadyToStart = performInitialSeek,
},
playerSettingsOverlay = new PlayerSettingsOverlay
replaySettingsOverlay = new ReplaySettingsOverlay
{
Alpha = 0,
}
@@ -189,9 +189,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private void updateVisibility()
{
if (configSettingsOverlay.Value)
playerSettingsOverlay.Show();
replaySettingsOverlay.Show();
else
playerSettingsOverlay.Hide();
replaySettingsOverlay.Hide();
}
protected override void Update()
@@ -0,0 +1,86 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Play.HUD
{
public partial class ReplayOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public ReplaySettingsOverlay Settings { get; private set; } = null!;
private const int fade_duration = 200;
private Bindable<bool> configSettingsOverlay = null!;
private Container messageContainer = null!;
private Container content = null!;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
RelativeSizeAxes = Axes.Both;
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
InternalChild = content = new Container
{
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
messageContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
},
Settings = new ReplaySettingsOverlay(),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
configSettingsOverlay.BindValueChanged(_ => updateVisibility(), true);
}
private void updateVisibility()
{
if (configSettingsOverlay.Value)
content.FadeIn(fade_duration, Easing.OutQuint);
else
content.FadeOut(fade_duration, Easing.OutQuint);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)
return false;
switch (e.Action)
{
case GlobalAction.ToggleReplaySettings:
configSettingsOverlay.Value = !configSettingsOverlay.Value;
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
public override void Show() => this.FadeIn(fade_duration, Easing.OutQuint);
public override void Hide() => this.FadeOut(fade_duration, Easing.OutQuint);
public void SetMessage(ScrollingMessage scrollingMessage) => messageContainer.Child = scrollingMessage;
}
}
@@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
public partial class PlayerSettingsOverlay : ExpandingContainer
public partial class ReplaySettingsOverlay : ExpandingContainer
{
private const float padding = 10;
@@ -27,11 +27,6 @@ namespace osu.Game.Screens.Play.HUD
private const float player_settings_width = 270;
private const int fade_duration = 200;
public override void Show() => this.FadeIn(fade_duration);
public override void Hide() => this.FadeOut(fade_duration);
// we'll handle this ourselves because we have slightly custom logic.
protected override bool ExpandOnHover => false;
@@ -52,7 +47,7 @@ namespace osu.Game.Screens.Play.HUD
// while collapsed down, so let's avoid that.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
public PlayerSettingsOverlay()
public ReplaySettingsOverlay()
: base(0, EXPANDED_WIDTH)
{
Origin = Anchor.TopRight;
@@ -81,11 +76,14 @@ namespace osu.Game.Screens.Play.HUD
Action = () => Expanded.Toggle()
});
AddInternal(new Box
AddRangeInternal(new Drawable[]
{
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
new Box
{
Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0), Color4.Black.Opacity(0.8f)),
Depth = float.MaxValue,
RelativeSizeAxes = Axes.Both,
},
});
}
+2 -33
View File
@@ -38,11 +38,6 @@ namespace osu.Game.Screens.Play
public const Easing FADE_EASING = Easing.OutQuint;
/// <summary>
/// The total height of all the bottom of screen scoring elements.
/// </summary>
public float BottomScoringElementsHeight { get; private set; }
protected override bool ShouldBeConsideredForInput(Drawable child)
{
// HUD uses AlwaysVisible on child components so they can be in an updated state for next display.
@@ -56,7 +51,6 @@ namespace osu.Game.Screens.Play
public readonly ModDisplay ModDisplay;
public readonly HoldForMenuButton HoldToQuit;
public readonly PlayerSettingsOverlay PlayerSettingsOverlay;
[Cached]
private readonly ClicksPerSecondController clicksPerSecondController;
@@ -82,7 +76,6 @@ namespace osu.Game.Screens.Play
private Bindable<HUDVisibilityMode> configVisibilityMode;
private Bindable<bool> configLeaderboardVisibility;
private Bindable<bool> configSettingsOverlay;
private readonly BindableBool replayLoaded = new BindableBool();
@@ -116,8 +109,6 @@ namespace osu.Game.Screens.Play
public HUDOverlay([CanBeNull] DrawableRuleset drawableRuleset, IReadOnlyList<Mod> mods, PlayerConfiguration configuration)
{
Container rightSettings;
this.drawableRuleset = drawableRuleset;
this.mods = mods;
this.configuration = configuration;
@@ -170,17 +161,6 @@ namespace osu.Game.Screens.Play
HoldToQuit = CreateHoldForMenuButton(),
}
},
rightSettings = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
PlayerSettingsOverlay = new PlayerSettingsOverlay
{
Alpha = 0,
}
}
},
TopLeftElements = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
@@ -190,7 +170,7 @@ namespace osu.Game.Screens.Play
},
};
hideTargets = new List<Drawable> { mainComponents, TopRightElements, rightSettings };
hideTargets = new List<Drawable> { mainComponents, TopRightElements };
if (rulesetComponents != null)
hideTargets.Add(rulesetComponents);
@@ -210,7 +190,6 @@ namespace osu.Game.Screens.Play
configVisibilityMode = config.GetBindable<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode);
configLeaderboardVisibility = config.GetBindable<bool>(OsuSetting.GameplayLeaderboard);
configSettingsOverlay = config.GetBindable<bool>(OsuSetting.ReplaySettingsOverlay);
if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce)
{
@@ -238,7 +217,6 @@ namespace osu.Game.Screens.Play
holdingForHUD.BindValueChanged(_ => updateVisibility());
IsPlaying.BindValueChanged(_ => updateVisibility());
configVisibilityMode.BindValueChanged(_ => updateVisibility());
configSettingsOverlay.BindValueChanged(_ => updateVisibility());
replayLoaded.BindValueChanged(e =>
{
@@ -295,7 +273,7 @@ namespace osu.Game.Screens.Play
TopLeftElements.Y = 0;
if (highestBottomScreenSpace.HasValue && DrawHeight - BottomRightElements.DrawHeight > 0)
BottomRightElements.Y = BottomScoringElementsHeight = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
BottomRightElements.Y = -Math.Clamp(DrawHeight - ToLocalSpace(highestBottomScreenSpace.Value).Y, 0, DrawHeight - BottomRightElements.DrawHeight);
else
BottomRightElements.Y = 0;
@@ -349,11 +327,6 @@ namespace osu.Game.Screens.Play
private void updateVisibility()
{
if (configSettingsOverlay.Value && replayLoaded.Value)
PlayerSettingsOverlay.Show();
else
PlayerSettingsOverlay.Hide();
if (ShowHud.Disabled)
return;
@@ -415,10 +388,6 @@ namespace osu.Game.Screens.Play
switch (e.Action)
{
case GlobalAction.ToggleReplaySettings:
configSettingsOverlay.Value = !configSettingsOverlay.Value;
return true;
case GlobalAction.HoldForHUD:
holdingForHUD.Value = true;
return false;
+19 -27
View File
@@ -320,32 +320,32 @@ namespace osu.Game.Screens.Play
OnRetry = Configuration.AllowUserInteraction ? () => Restart() : null,
OnQuit = () => PerformExitWithConfirmation(),
},
exitOverlay = new HotkeyExitOverlay
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
PerformExit(skipTransition: true);
},
},
});
if (cancellationToken.IsCancellationRequested)
return;
GameplayClockContainer.Add(exitOverlay = new HotkeyExitOverlay
{
Depth = float.MinValue,
Action = () =>
{
if (!this.IsCurrentScreen()) return;
PerformExit(skipTransition: true);
},
});
if (Configuration.AllowRestart)
{
rulesetSkinProvider.AddRange(new Drawable[]
GameplayClockContainer.Add(retryOverlay = new HotkeyRetryOverlay
{
retryOverlay = new HotkeyRetryOverlay
Depth = float.MinValue,
Action = () =>
{
Action = () =>
{
if (!this.IsCurrentScreen()) return;
if (!this.IsCurrentScreen()) return;
Restart(true);
},
Restart(true);
},
});
}
@@ -361,6 +361,9 @@ namespace osu.Game.Screens.Play
// we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there.
failAnimationContainer.Add(createOverlayComponents());
// Used by ReplaySettingsOverlay for button positioning.
dependencies.CacheAs(HUDOverlay);
if (!DrawableRuleset.AllowGameplayOverlays)
{
HUDOverlay.ShowHud.Value = false;
@@ -426,16 +429,6 @@ namespace osu.Game.Screens.Play
IsBreakTime.BindValueChanged(onBreakTimeChanged, true);
}
/// <summary>
/// Components which were created via <see cref="CreateOverlayComponents"/>.
/// </summary>
public Drawable OverlayComponents { get; private set; }
/// <summary>
/// Implement to add any components which should exist above gameplay but below the HUD.
/// </summary>
protected virtual Drawable CreateOverlayComponents() => Empty();
protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart);
private Drawable createUnderlayComponents(WorkingBeatmap working)
@@ -484,7 +477,6 @@ namespace osu.Game.Screens.Play
Children = new[]
{
DimmableStoryboard.OverlayLayerContainer.CreateProxy(),
OverlayComponents = CreateOverlayComponents(),
HUDOverlay = new HUDOverlay(DrawableRuleset, GameplayState.Mods, Configuration)
{
HoldToQuit =
+25 -23
View File
@@ -17,6 +17,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.Leaderboards;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
@@ -47,6 +48,8 @@ namespace osu.Game.Screens.Play
private ReplayFailIndicator? failIndicator;
private PlaybackSettings? playbackSettings;
public ReplayOverlay ReplayOverlay { get; private set; } = null!;
protected override bool CheckModsAllowFailure()
{
// autoplay should be able to fail if the beatmap is not humanly beatable
@@ -80,7 +83,7 @@ namespace osu.Game.Screens.Play
/// Add a settings group to the HUD overlay. Intended to be used by rulesets to add replay-specific settings.
/// </summary>
/// <param name="settings">The settings group to be shown.</param>
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => HUDOverlay.PlayerSettingsOverlay.Add(settings));
public void AddSettings(PlayerSettingsGroup settings) => Schedule(() => ReplayOverlay.Settings.Add(settings));
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
@@ -90,6 +93,8 @@ namespace osu.Game.Screens.Play
AddInternal(leaderboardProvider);
GameplayClockContainer.Add(ReplayOverlay = new ReplayOverlay());
playbackSettings = new PlaybackSettings
{
Depth = float.MaxValue,
@@ -99,7 +104,25 @@ namespace osu.Game.Screens.Play
if (GameplayClockContainer is MasterGameplayClockContainer master)
playbackSettings.UserPlaybackRate.BindTo(master.UserPlaybackRate);
HUDOverlay.PlayerSettingsOverlay.AddAtStart(playbackSettings);
ReplayOverlay.Settings.AddAtStart(playbackSettings);
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" on ");
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
});
ReplayOverlay.SetMessage(new ScrollingMessage(message)
{
Y = 96,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
AddInternal(new RulesetSkinProvidingContainer(GameplayState.Ruleset, GameplayState.Beatmap, Beatmap.Value.Skin)
{
@@ -117,27 +140,6 @@ namespace osu.Game.Screens.Play
});
}
protected override Drawable CreateOverlayComponents()
{
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" on ");
message.AddArbitraryDrawable(new PlayedOnText(Score.ScoreInfo.Date, false)
{
Font = OsuFont.Style.Body.With(weight: FontWeight.SemiBold),
});
return new ScrollingMessage(message)
{
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
};
}
protected override void PrepareReplay()
{
DrawableRuleset?.SetReplayScore(Score);
+25 -15
View File
@@ -12,6 +12,7 @@ using osu.Game.Online.Spectator;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.Play
@@ -23,29 +24,38 @@ namespace osu.Game.Screens.Play
private readonly Score score;
public bool ShowSettingsOverlay { get; init; } = true;
protected SpectatorPlayer(Score score, PlayerConfiguration? configuration = null)
: base(configuration)
{
this.score = score;
}
protected override Drawable CreateOverlayComponents()
[BackgroundDependencyLoader]
private void load()
{
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
return new ScrollingMessage(message)
if (ShowSettingsOverlay)
{
Y = 100,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
};
var replayOverlay = new ReplayOverlay();
GameplayClockContainer.Add(replayOverlay);
// TODO: This should be customised for `MultiplayerSpectatorPlayer` to be static and only show the player name.
// Or maybe we should completely redesign this to show the user avatar and other things if that happens.
OsuTextFlowContainer message = new OsuTextFlowContainer(cp => cp.Font = OsuFont.Style.Body) { AutoSizeAxes = Axes.Both };
message.AddText("Watching ");
message.AddText(Score.ScoreInfo.User.Username, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" play ");
message.AddText(Beatmap.Value.BeatmapInfo.GetDisplayTitleRomanisable(), s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
message.AddText(" live", s => s.Font = s.Font.With(weight: FontWeight.Bold));
replayOverlay.SetMessage(new ScrollingMessage(message)
{
Y = 96,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
});
}
}
protected override void LoadComplete()
@@ -860,6 +860,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
public async Task PlayUserCard(int userId, Func<RankedPlayCardItem[], RankedPlayCardItem> selector)
{
RankedPlayCardItem card = selector(((RankedPlayRoomState)ServerRoom!.MatchState!).Users[userId].Hand.ToArray());
MultiplayerPlaylistItem? item = GetCardWithPlaylistItem(card).PlaylistItem.Value;
if (item != null)
{
ServerRoom!.Playlist.Add(item);
await ((IMultiplayerClient)this).PlaylistItemAdded(clone(item)).ConfigureAwait(false);
await ((IMultiplayerClient)this).PlaylistItemChanged(clone(item)).ConfigureAwait(false);
var settings = clone(ServerRoom!.Settings);
settings.PlaylistItemId = item.ID;
await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false);
}
await ((IRankedPlayClient)this).RankedPlayCardPlayed(clone(card)).ConfigureAwait(false);
}
+1 -1
View File
@@ -40,7 +40,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="20.1.0" />
<PackageReference Include="ppy.osu.Framework" Version="2026.318.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.331.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2026.404.0" />
<PackageReference Include="Sentry" Version="6.2.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.47.0" />