1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-27 00:23:01 +08:00

Merge branch 'master' into fix-realm-post-storage-migration-failure

This commit is contained in:
Bartłomiej Dach 2021-12-17 00:19:46 +01:00 committed by GitHub
commit 54790bb758
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 955 additions and 348 deletions

View File

@ -1,6 +1,6 @@
# Contributing Guidelines
Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
@ -32,7 +32,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
* **Provide more information when asked to do so.**
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local lazer database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
* **When submitting a feature proposal, please describe it in the most understandable way you can.**
@ -54,7 +54,7 @@ Issues, bug reports and feature suggestions are welcomed, though please keep in
We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
However, do keep in mind that the core team is committed to bringing osu!lazer up to par with stable first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
Here are some key things to note before jumping in:
@ -128,7 +128,7 @@ Here are some key things to note before jumping in:
* **Don't mistake criticism of code for criticism of your person.**
As mentioned before, we are highly committed to quality when it comes to the lazer project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
* **Feel free to reach out for help.**

View File

@ -11,7 +11,7 @@
A free-to-win rhythm game. Rhythm is just a *click* away!
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the codename "*lazer*". As in sharper than cutting-edge.
The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Currently known by and released under the release codename "*lazer*". As in sharper than cutting-edge.
## Status

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1210.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1215.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -55,73 +55,75 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
bool firstDeltaSwitch = false;
for (int i = Previous.Count - 2; i > 0; i--)
int rhythmStart = 0;
while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
rhythmStart++;
for (int i = rhythmStart; i > 0; i--)
{
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
double currHistoricalDecay = Math.Max(0, (history_time_max - (current.StartTime - currObj.StartTime))) / history_time_max; // scales note 0 to 1 from history to now
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
if (currHistoricalDecay != 0)
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
{
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
windowPenalty = Math.Min(1, windowPenalty);
double effectiveRatio = windowPenalty * currRatio;
if (firstDeltaSwitch)
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
{
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else
{
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
if (islandSize < 7)
islandSize++; // island is still progressing, count size.
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
else
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
effectiveRatio *= 0.125;
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
effectiveRatio *= 0.25;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
effectiveRatio *= 0.50;
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size.
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1;
}
}
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
{
// Begin counting island until we change speed again.
firstDeltaSwitch = true;
startRatio = effectiveRatio;
islandSize = 1;
}
}
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)

View File

@ -3,8 +3,10 @@
using System.Collections.Generic;
using System.ComponentModel;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@ -39,6 +41,19 @@ namespace osu.Game.Rulesets.Osu
return base.Handle(e);
}
protected override bool HandleMouseTouchStateChange(TouchStateChangeEvent e)
{
if (!AllowUserCursorMovement)
{
// Still allow for forwarding of the "touch" part, but replace the positional data with that of the mouse.
// Primarily relied upon by the "autopilot" osu! mod.
var touch = new Touch(e.Touch.Source, CurrentState.Mouse.Position);
e = new TouchStateChangeEvent(e.State, e.Input, touch, e.IsActive, null);
}
return base.HandleMouseTouchStateChange(e);
}
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
public bool AllowUserPresses = true;

View File

@ -45,8 +45,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddRepeatStep("add some users", () => Client.AddUser(new APIUser { Id = id++ }), 5);
checkPlayingUserCount(0);
AddAssert("playlist item is available", () => Client.CurrentMatchPlayingItem.Value != null);
changeState(3, MultiplayerUserState.WaitingForLoad);
checkPlayingUserCount(3);
@ -64,8 +62,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
AddStep("leave room", () => Client.LeaveRoom());
checkPlayingUserCount(0);
AddAssert("playlist item is null", () => Client.CurrentMatchPlayingItem.Value == null);
}
[Test]

View File

@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Online.Chat;
namespace osu.Game.Tests.Online.Chat
{
[TestFixture]
public class MessageNotifierTest
{
[Test]
public void TestContainsUsernameMidlinePositive()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test message", "Test"));
}
[Test]
public void TestContainsUsernameStartOfLinePositive()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test message", "Test"));
}
[Test]
public void TestContainsUsernameEndOfLinePositive()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("This is a test", "Test"));
}
[Test]
public void TestContainsUsernameMidlineNegative()
{
Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a testmessage for notifications", "Test"));
}
[Test]
public void TestContainsUsernameStartOfLineNegative()
{
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Testmessage", "Test"));
}
[Test]
public void TestContainsUsernameEndOfLineNegative()
{
Assert.IsFalse(MessageNotifier.CheckContainsUsername("This is a notificationtest", "Test"));
}
[Test]
public void TestContainsUsernameBetweenInterpunction()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Hello 'test'-message", "Test"));
}
[Test]
public void TestContainsUsernameUnicode()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test \u0460\u0460 message", "\u0460\u0460"));
}
[Test]
public void TestContainsUsernameUnicodeNegative()
{
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test ha\u0460\u0460o message", "\u0460\u0460"));
}
[Test]
public void TestContainsUsernameSpecialCharactersPositive()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("Test [#^-^#] message", "[#^-^#]"));
}
[Test]
public void TestContainsUsernameSpecialCharactersNegative()
{
Assert.IsFalse(MessageNotifier.CheckContainsUsername("Test pad[#^-^#]oru message", "[#^-^#]"));
}
[Test]
public void TestContainsUsernameAtSign()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("@username hi", "username"));
}
[Test]
public void TestContainsUsernameColon()
{
Assert.IsTrue(MessageNotifier.CheckContainsUsername("username: hi", "username"));
}
}
}

View File

@ -114,18 +114,23 @@ namespace osu.Game.Tests.Online
public void TestTrackerRespectsChecksum()
{
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).Wait());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
AddStep("import altered beatmap", () =>
{
beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait();
});
addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded);
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("recreate tracker", () => Child = availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem }
});
addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded);
AddStep("reimport original beatmap", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait());
addAvailabilityCheckStep("locally available after re-import", BeatmapAvailability.LocallyAvailable);
}
private void addAvailabilityCheckStep(string description, Func<BeatmapAvailability> expected)

View File

@ -5,8 +5,11 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.Backgrounds;
@ -15,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Screens;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.Background
{
@ -22,8 +26,7 @@ namespace osu.Game.Tests.Visual.Background
public class TestSceneBackgroundScreenDefault : OsuTestScene
{
private BackgroundScreenStack stack;
private BackgroundScreenDefault screen;
private TestBackgroundScreenDefault screen;
private Graphics.Backgrounds.Background getCurrentBackground() => screen.ChildrenOfType<Graphics.Backgrounds.Background>().FirstOrDefault();
[Resolved]
@ -36,10 +39,95 @@ namespace osu.Game.Tests.Visual.Background
public void SetUpSteps()
{
AddStep("create background stack", () => Child = stack = new BackgroundScreenStack());
AddStep("push default screen", () => stack.Push(screen = new BackgroundScreenDefault(false)));
AddStep("push default screen", () => stack.Push(screen = new TestBackgroundScreenDefault()));
AddUntilStep("wait for screen to load", () => screen.IsCurrentScreen());
}
[Test]
public void TestBeatmapBackgroundTracksBeatmap()
{
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
Graphics.Backgrounds.Background last = null;
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
AddStep("store background", () => last = getCurrentBackground());
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
AddStep("store background", () => last = getCurrentBackground());
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddUntilStep("wait for beatmap background to change", () => screen.CheckLastLoadChange() == true);
AddUntilStep("background is new beatmap background", () => last != getCurrentBackground());
}
[Test]
public void TestBeatmapBackgroundTracksBeatmapWhenSuspended()
{
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
BackgroundScreenBeatmap nestedScreen = null;
// of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("pop screen back to top level", () => screen.MakeCurrent());
AddAssert("top level background changed", () => screen.CheckLastLoadChange() == true);
}
[Test]
public void TestBeatmapBackgroundIgnoresNoChangeWhenSuspended()
{
BackgroundScreenBeatmap nestedScreen = null;
WorkingBeatmap originalWorking = null;
setSupporter(true);
setSourceMode(BackgroundSource.Beatmap);
AddStep("change beatmap", () => originalWorking = Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddAssert("background changed", () => screen.CheckLastLoadChange() == true);
AddUntilStep("wait for beatmap background to be loaded", () => getCurrentBackground()?.GetType() == typeof(BeatmapBackground));
// of note, this needs to be a type that doesn't match BackgroundScreenDefault else it is silently not pushed by the background stack.
AddStep("push new background to stack", () => stack.Push(nestedScreen = new BackgroundScreenBeatmap(Beatmap.Value)));
AddUntilStep("wait for screen to load", () => nestedScreen.IsLoaded && nestedScreen.IsCurrentScreen());
// we're testing a case where scheduling may be used to avoid issues, so ensure the scheduler is no longer running.
AddUntilStep("wait for top level not alive", () => !screen.IsAlive);
AddStep("change beatmap", () => Beatmap.Value = createTestWorkingBeatmapWithUniqueBackground());
AddStep("change beatmap back", () => Beatmap.Value = originalWorking);
AddAssert("top level background hasn't changed yet", () => screen.CheckLastLoadChange() == null);
AddStep("pop screen back to top level", () => screen.MakeCurrent());
AddStep("top level screen is current", () => screen.IsCurrentScreen());
AddAssert("top level background reused existing", () => screen.CheckLastLoadChange() == false);
}
[Test]
public void TestBackgroundTypeSwitch()
{
@ -78,36 +166,24 @@ namespace osu.Game.Tests.Visual.Background
[TestCase(BackgroundSource.Skin, typeof(SkinBackground))]
public void TestBackgroundDoesntReloadOnNoChange(BackgroundSource source, Type backgroundType)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(source);
setSupporter(true);
if (source == BackgroundSource.Skin)
setCustomSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == backgroundType);
AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == backgroundType);
AddAssert("next doesn't load new background", () => screen.Next() == false);
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure same background instance", () => last == getCurrentBackground());
}
[Test]
public void TestBackgroundCyclingOnDefaultSkin([Values] bool supporter)
{
Graphics.Backgrounds.Background last = null;
setSourceMode(BackgroundSource.Skin);
setSupporter(supporter);
setDefaultSkin();
AddUntilStep("wait for beatmap background to be loaded", () => (last = getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddUntilStep("wait for beatmap background to be loaded", () => (getCurrentBackground())?.GetType() == typeof(Graphics.Backgrounds.Background));
AddAssert("next cycles background", () => screen.Next());
// doesn't really need to be checked but might as well.
AddWaitStep("wait a bit", 5);
AddUntilStep("ensure different background instance", () => last != getCurrentBackground());
}
private void setSourceMode(BackgroundSource source) =>
@ -120,6 +196,42 @@ namespace osu.Game.Tests.Visual.Background
Id = API.LocalUser.Value.Id + 1,
});
private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio);
private class TestBackgroundScreenDefault : BackgroundScreenDefault
{
private bool? lastLoadTriggerCausedChange;
public TestBackgroundScreenDefault()
: base(false)
{
}
public override bool Next()
{
bool didChange = base.Next();
lastLoadTriggerCausedChange = didChange;
return didChange;
}
public bool? CheckLastLoadChange()
{
bool? lastChange = lastLoadTriggerCausedChange;
lastLoadTriggerCausedChange = null;
return lastChange;
}
}
private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
{
public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager)
: base(new Beatmap(), null, audioManager)
{
}
protected override Texture GetBackground() => new Texture(1, 1);
}
private void setCustomSkin()
{
// feign a skin switch. this doesn't do anything except force CurrentSkin to become a LegacySkin.

View File

@ -6,12 +6,12 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
@ -19,11 +19,10 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osuTK;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Tests.Visual.Beatmaps
{
public class TestSceneBeatmapCard : OsuTestScene
public class TestSceneBeatmapCard : OsuManualInputManagerTestScene
{
/// <summary>
/// All cards on this scene use a common online ID to ensure that map download, preview tracks, etc. can be tested manually with online sources.
@ -253,14 +252,32 @@ namespace osu.Game.Tests.Visual.Beatmaps
public void TestNormal()
{
createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
}
AddToggleStep("toggle expanded state", expanded =>
{
var card = this.ChildrenOfType<BeatmapCard>().Last();
if (!card.Expanded.Disabled)
card.Expanded.Value = expanded;
});
AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType<BeatmapCard>().ForEach(card => card.Expanded.Disabled = disabled));
[Test]
public void TestHoverState()
{
AddStep("create cards", () => Child = createContent(OverlayColourScheme.Blue, s => new BeatmapCard(s)));
AddStep("Hover card", () => InputManager.MoveMouseTo(firstCard()));
AddWaitStep("wait for potential state change", 5);
AddAssert("card is not expanded", () => !firstCard().Expanded.Value);
AddStep("Hover spectrum display", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType<DifficultySpectrumDisplay>().Single()));
AddUntilStep("card is expanded", () => firstCard().Expanded.Value);
AddStep("Hover difficulty content", () => InputManager.MoveMouseTo(firstCard().ChildrenOfType<BeatmapCardDifficultyList>().Single()));
AddWaitStep("wait for potential state change", 5);
AddAssert("card is still expanded", () => firstCard().Expanded.Value);
AddStep("Hover main content again", () => InputManager.MoveMouseTo(firstCard()));
AddWaitStep("wait for potential state change", 5);
AddAssert("card is still expanded", () => firstCard().Expanded.Value);
AddStep("Hover away", () => InputManager.MoveMouseTo(this.ChildrenOfType<BeatmapCard>().Last()));
AddUntilStep("card is not expanded", () => !firstCard().Expanded.Value);
BeatmapCard firstCard() => this.ChildrenOfType<BeatmapCard>().First();
}
}
}

View File

@ -85,11 +85,12 @@ namespace osu.Game.Tests.Visual.Gameplay
loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1);
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
double targetTime = addEventToLoop ? 20000 : 0;
target.Alpha.Add(Easing.None, targetTime + firstStoryboardEvent, targetTime + firstStoryboardEvent + 500, 0, 1);
// these should be ignored due to being in the future.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
storyboard.GetLayer("Background").Add(sprite);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -27,13 +27,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
addItem(() => OtherBeatmap);
AddAssert("playlist has 2 items", () => Client.APIRoom?.Playlist.Count == 2);
AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[1].Beatmap.Value.OnlineID == OtherBeatmap.OnlineID);
addItem(() => InitialBeatmap);
AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3);
AddAssert("last playlist item is different", () => Client.APIRoom?.Playlist[2].Beatmap.Value.OnlineID == InitialBeatmap.OnlineID);
AddAssert("first item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("first item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -43,7 +41,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("playlist has only one item", () => Client.APIRoom?.Playlist.Count == 1);
AddAssert("playlist item is expired", () => Client.APIRoom?.Playlist[0].Expired == true);
AddAssert("last item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("last item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -55,12 +53,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
RunGameplay();
AddAssert("first item expired", () => Client.APIRoom?.Playlist[0].Expired == true);
AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID);
AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID);
RunGameplay();
AddAssert("second item expired", () => Client.APIRoom?.Playlist[1].Expired == true);
AddAssert("next item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[2].ID);
AddAssert("next item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[2].ID);
}
[Test]
@ -74,15 +72,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("change queue mode", () => Client.ChangeSettings(queueMode: QueueMode.HostOnly));
AddAssert("playlist has 3 items", () => Client.APIRoom?.Playlist.Count == 3);
AddAssert("playlist item is the other beatmap", () => Client.CurrentMatchPlayingItem.Value?.BeatmapID == OtherBeatmap.OnlineID);
AddAssert("playlist item is not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
AddAssert("item 2 is not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
AddAssert("current item is the other beatmap", () => Client.Room?.Settings.PlaylistItemId == 2);
}
[Test]
public void TestCorrectItemSelectedAfterNewItemAdded()
{
addItem(() => OtherBeatmap);
AddAssert("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)

View File

@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@ -20,7 +21,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFirstItemSelectedByDefault()
{
AddAssert("first item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("first item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -28,7 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => InitialBeatmap);
AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -36,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
selectNewItem(() => OtherBeatmap);
AddAssert("playlist item still selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[0].ID);
AddAssert("playlist item still selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[0].ID);
}
[Test]
@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2);
AddAssert("first playlist item expired", () => Client.APIRoom?.Playlist[0].Expired == true);
AddAssert("second playlist item not expired", () => Client.APIRoom?.Playlist[1].Expired == false);
AddAssert("second playlist item selected", () => Client.CurrentMatchPlayingItem.Value?.ID == Client.APIRoom?.Playlist[1].ID);
AddAssert("second playlist item selected", () => Client.Room?.Settings.PlaylistItemId == Client.APIRoom?.Playlist[1].ID);
}
[Test]
@ -85,6 +86,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void selectNewItem(Func<BeatmapInfo> beatmap)
{
AddUntilStep("wait for playlist panels to load", () =>
{
var queueList = this.ChildrenOfType<MultiplayerQueueList>().Single();
return queueList.ChildrenOfType<DrawableRoomPlaylistItem>().Count() == queueList.Items.Count;
});
AddStep("click edit button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().First());
@ -97,7 +104,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(otherBeatmap = beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID);
AddUntilStep("selected item is new beatmap", () => (CurrentSubScreen as MultiplayerMatchSubScreen)?.SelectedItem.Value?.BeatmapID == otherBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)

View File

@ -30,6 +30,8 @@ using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Spectate;
@ -391,9 +393,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddStep("set user ready", () => client.ChangeState(MultiplayerUserState.Ready));
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
pressReadyButton();
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
@ -413,10 +415,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
pressReadyButton();
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value);
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.Room?.Settings.PlaylistItemId);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
@ -592,20 +596,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("click ready button", () =>
{
InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready);
AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value);
AddStep("click start button", () => InputManager.Click(MouseButton.Left));
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
enterGameplay();
// Gameplay runs in real-time, so we need to incrementally check if gameplay has finished in order to not time out.
for (double i = 1000; i < TestResources.QUICK_BEATMAP_LENGTH; i += 1000)
@ -665,7 +656,173 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
private MultiplayerReadyButton readyButton => this.ChildrenOfType<MultiplayerReadyButton>().Single();
[Test]
public void TestSpectatingStateResetOnBackButtonDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("press back button and exit", () =>
{
multiplayerScreenStack.OnBackButton();
multiplayerScreenStack.Exit();
});
AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen());
AddUntilStep("user state is idle", () => client.LocalUser?.State == MultiplayerUserState.Idle);
}
[Test]
public void TestSpectatingStateNotResetOnBackButtonOutsideOfGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("set spectating state", () => client.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
AddUntilStep("state set to spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user ready", () => client.ChangeUserState(1234, MultiplayerUserState.Ready));
pressReadyButton(1234);
AddUntilStep("wait for gameplay", () => (multiplayerScreenStack.CurrentScreen as MultiSpectatorScreen)?.IsLoaded == true);
AddStep("set other user loaded", () => client.ChangeUserState(1234, MultiplayerUserState.Loaded));
AddStep("set other user finished play", () => client.ChangeUserState(1234, MultiplayerUserState.FinishedPlay));
AddStep("press back button and exit", () =>
{
multiplayerScreenStack.OnBackButton();
multiplayerScreenStack.Exit();
});
AddUntilStep("wait for return to match subscreen", () => multiplayerScreenStack.MultiplayerScreen.IsCurrentScreen());
AddWaitStep("wait for possible state change", 5);
AddUntilStep("user state is spectating", () => client.LocalUser?.State == MultiplayerUserState.Spectating);
}
[Test]
public void TestItemAddedByOtherUserDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
enterGameplay();
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
{
BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
})));
AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue contains item", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Single().ID == 2);
}
[Test]
public void TestItemAddedAndDeletedByOtherUserDuringGameplay()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
enterGameplay();
AddStep("join other user", () => client.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => client.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(new PlaylistItem
{
BeatmapID = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo.OnlineID ?? -1
})));
AddUntilStep("item arrived in playlist", () => client.Room?.Playlist.Count == 2);
AddStep("delete item as other user", () => client.RemoveUserPlaylistItem(1234, 2));
AddUntilStep("item removed from playlist", () => client.Room?.Playlist.Count == 1);
AddStep("exit gameplay as initial user", () => multiplayerScreenStack.MultiplayerScreen.MakeCurrent());
AddUntilStep("queue is empty", () => this.ChildrenOfType<MultiplayerQueueList>().Single().Items.Count == 0);
}
private void enterGameplay()
{
pressReadyButton();
pressReadyButton();
AddUntilStep("wait for player", () => multiplayerScreenStack.CurrentScreen is Player);
}
private ReadyButton readyButton => this.ChildrenOfType<ReadyButton>().Single();
private void pressReadyButton(int? playingUserId = null)
{
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
MultiplayerUserState lastState = MultiplayerUserState.Idle;
MultiplayerRoomUser user = null;
AddStep("click ready button", () =>
{
user = playingUserId == null ? client.LocalUser : client.Room?.Users.Single(u => u.UserID == playingUserId);
lastState = user?.State ?? MultiplayerUserState.Idle;
InputManager.MoveMouseTo(readyButton);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for state change", () => user?.State != lastState);
}
private void createRoom(Func<Room> room)
{

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
@ -27,7 +28,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("initialise gameplay", () =>
{
Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, Client.CurrentMatchPlayingItem.Value, Client.Room?.Users.ToArray()));
Stack.Push(player = new MultiplayerPlayer(Client.APIRoom, new PlaylistItem
{
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }
}, Client.Room?.Users.ToArray()));
});
AddUntilStep("wait for player to be current", () => player.IsCurrentScreen() && player.IsLoaded);

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
@ -27,11 +28,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerQueueList : MultiplayerTestScene
{
private MultiplayerQueueList playlist;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
[Cached(typeof(UserLookupCache))]
private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache();
private MultiplayerQueueList playlist;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
@ -50,12 +52,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create playlist", () =>
{
selectedItem.Value = null;
Child = playlist = new MultiplayerQueueList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
SelectedItem = { BindTarget = Client.CurrentMatchPlayingItem },
SelectedItem = { BindTarget = selectedItem },
Items = { BindTarget = Client.APIRoom!.Playlist }
};
});
@ -107,22 +111,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
assertDeleteButtonVisibility(0, false);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
AddStep("select item 0", () => selectedItem.Value = playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().ElementAt(0).Model);
assertDeleteButtonVisibility(0, false);
assertDeleteButtonVisibility(1, true);
// Run through gameplay.
AddStep("set state to ready", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Ready));
AddUntilStep("local state is ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
AddStep("start match", () => Client.StartMatch());
AddUntilStep("match started", () => Client.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
AddStep("set state to loaded", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Loaded));
AddUntilStep("local state is playing", () => Client.LocalUser?.State == MultiplayerUserState.Playing);
AddStep("set state to finished play", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.FinishedPlay));
AddUntilStep("local state is results", () => Client.LocalUser?.State == MultiplayerUserState.Results);
AddStep("select item 1", () => selectedItem.Value = playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().ElementAt(1).Model);
assertDeleteButtonVisibility(0, true);
assertDeleteButtonVisibility(1, false);
}

View File

@ -393,6 +393,25 @@ namespace osu.Game.Tests.Visual.Online
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
}
[Test]
public void TestMultiplayerChannelIsNotShown()
{
Channel multiplayerChannel = null;
AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
Type = ChannelType.Multiplayer,
}));
AddAssert("channel joined", () => channelManager.JoinedChannels.Contains(multiplayerChannel));
AddAssert("channel not present in overlay", () => !chatOverlay.TabMap.ContainsKey(multiplayerChannel));
AddAssert("multiplayer channel is not current", () => channelManager.CurrentChannel.Value != multiplayerChannel);
AddStep("leave channel", () => channelManager.LeaveChannel(multiplayerChannel));
AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
}
private void pressChannelHotkey(int number)
{
var channelKey = Key.Number0 + number;

View File

@ -15,9 +15,11 @@ using osu.Game.Database;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual.OnlinePlay;
using osuTK.Input;
@ -112,37 +114,80 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestBeatmapUpdatedOnReImport()
{
BeatmapSetInfo importedSet = null;
string realHash = null;
int realOnlineId = 0;
int realOnlineSetId = 0;
AddStep("import altered beatmap", () =>
AddStep("store real beatmap values", () =>
{
IBeatmap beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
beatmap.BeatmapInfo.BaseDifficulty.CircleSize = 1;
// intentionally increment online IDs to clash with import below.
beatmap.BeatmapInfo.OnlineID++;
beatmap.BeatmapInfo.BeatmapSet.OnlineID++;
importedSet = manager.Import(beatmap.BeatmapInfo.BeatmapSet).Result.Value;
realHash = importedBeatmap.Value.Beatmaps[0].MD5Hash;
realOnlineId = importedBeatmap.Value.Beatmaps[0].OnlineID ?? -1;
realOnlineSetId = importedBeatmap.Value.OnlineID ?? -1;
});
AddStep("import modified beatmap", () =>
{
var modifiedBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
OnlineID = realOnlineId,
BeatmapSet =
{
OnlineID = realOnlineSetId
}
},
};
modifiedBeatmap.HitObjects.Clear();
modifiedBeatmap.HitObjects.Add(new HitCircle { StartTime = 5000 });
manager.Import(modifiedBeatmap.BeatmapInfo.BeatmapSet).Wait();
});
// Create the room using the real beatmap values.
setupAndCreateRoom(room =>
{
room.Name.Value = "my awesome room";
room.Host.Value = API.LocalUser.Value;
room.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = importedSet.Beatmaps[0] },
Beatmap =
{
Value = new BeatmapInfo
{
MD5Hash = realHash,
OnlineID = realOnlineId,
BeatmapSet = new BeatmapSetInfo
{
OnlineID = realOnlineSetId,
}
}
},
Ruleset = { Value = new OsuRuleset().RulesetInfo }
});
});
AddAssert("match has altered beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize == 1);
AddAssert("match has default beatmap", () => match.Beatmap.IsDefault);
importBeatmap();
AddStep("reimport original beatmap", () =>
{
var originalBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
OnlineID = realOnlineId,
BeatmapSet =
{
OnlineID = realOnlineSetId
}
},
};
AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.Difficulty.CircleSize != 1);
manager.Import(originalBeatmap.BeatmapInfo.BeatmapSet).Wait();
});
AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash);
}
private void setupAndCreateRoom(Action<Room> room)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual
((DummyAPIAccess)api).HandleRequest = request => multiplayerScreen.RequestsHandler.HandleRequest(request, api.LocalUser.Value, game);
}
public override bool OnBackButton() => multiplayerScreen.OnBackButton();
public override bool OnBackButton() => (screenStack.CurrentScreen as OsuScreen)?.OnBackButton() ?? base.OnBackButton();
public override bool OnExiting(IScreen next)
{

View File

@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
public Bindable<bool> Expanded { get; } = new BindableBool();
public IBindable<bool> Expanded { get; }
private const float width = 408;
private const float height = 100;
@ -64,9 +64,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public BeatmapCard(APIBeatmapSet beatmapSet)
public BeatmapCard(APIBeatmapSet beatmapSet, bool allowExpansion = true)
: base(HoverSampleSet.Submit)
{
Expanded = new BindableBool { Disabled = !allowExpansion };
this.beatmapSet = beatmapSet;
favouriteState = new Bindable<BeatmapSetFavouriteState>(new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount));
downloadTracker = new BeatmapDownloadTracker(beatmapSet);
@ -282,15 +284,15 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Hovered = _ =>
{
content.ScheduleShow();
content.ExpandAfterDelay();
return false;
},
Unhovered = _ =>
{
// This hide should only trigger if the expanded content has not shown yet.
// ie. if the user has not shown intent to want to see it (quickly moved over the info row area).
// Handles the case where a user has not shown explicit intent to view expanded info.
// ie. quickly moved over the info row area but didn't remain within it.
if (!Expanded.Value)
content.ScheduleHide();
content.CancelExpand();
}
}
}
@ -366,8 +368,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override void OnHoverLost(HoverLostEvent e)
{
content.ScheduleHide();
updateState();
base.OnHoverLost(e);
}

View File

@ -31,7 +31,9 @@ namespace osu.Game.Beatmaps.Drawables.Cards
set => dropdownScroll.Child = value;
}
public Bindable<bool> Expanded { get; } = new BindableBool();
public IBindable<bool> Expanded => expanded;
private readonly BindableBool expanded = new BindableBool();
private readonly Box background;
private readonly Container content;
@ -54,7 +56,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
AutoSizeAxes = Axes.Y,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
Unhovered = _ => checkForHide(),
Unhovered = _ => updateFromHoverChange(),
Children = new Drawable[]
{
background = new Box
@ -76,10 +78,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Alpha = 0,
Hovered = _ =>
{
keep();
updateFromHoverChange();
return true;
},
Unhovered = _ => checkForHide(),
Unhovered = _ => updateFromHoverChange(),
Child = dropdownScroll = new ExpandedContentScrollContainer
{
RelativeSizeAxes = Axes.X,
@ -119,51 +121,20 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private ScheduledDelegate? scheduledExpandedChange;
public void ScheduleShow()
{
scheduledExpandedChange?.Cancel();
if (Expanded.Disabled || Expanded.Value)
return;
public void ExpandAfterDelay() => queueExpandedStateChange(true, 100);
scheduledExpandedChange = Scheduler.AddDelayed(() =>
{
if (!Expanded.Disabled)
Expanded.Value = true;
}, 100);
}
public void CancelExpand() => scheduledExpandedChange?.Cancel();
public void ScheduleHide()
{
scheduledExpandedChange?.Cancel();
if (Expanded.Disabled || !Expanded.Value)
return;
private void updateFromHoverChange() =>
queueExpandedStateChange(content.IsHovered || dropdownContent.IsHovered, 100);
scheduledExpandedChange = Scheduler.AddDelayed(() =>
{
if (!Expanded.Disabled)
Expanded.Value = false;
}, 500);
}
private void checkForHide()
{
if (Expanded.Disabled)
return;
if (content.IsHovered || dropdownContent.IsHovered)
return;
scheduledExpandedChange?.Cancel();
Expanded.Value = false;
}
private void keep()
private void queueExpandedStateChange(bool newState, int delay = 0)
{
if (Expanded.Disabled)
return;
scheduledExpandedChange?.Cancel();
Expanded.Value = true;
scheduledExpandedChange = Scheduler.AddDelayed(() => expanded.Value = newState, delay);
}
private void updateState()

View File

@ -72,18 +72,21 @@ namespace osu.Game.Graphics.Cursor
protected override bool OnMouseDown(MouseDownEvent e)
{
// only trigger animation for main mouse buttons
activeCursor.Scale = new Vector2(1);
activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint);
activeCursor.AdditiveLayer.Alpha = 0;
activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint);
if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating)
if (State.Value == Visibility.Visible)
{
// if cursor is already rotating don't reset its rotate origin
dragRotationState = DragRotationState.DragStarted;
positionMouseDown = e.MousePosition;
// only trigger animation for main mouse buttons
activeCursor.Scale = new Vector2(1);
activeCursor.ScaleTo(0.90f, 800, Easing.OutQuint);
activeCursor.AdditiveLayer.Alpha = 0;
activeCursor.AdditiveLayer.FadeInFromZero(800, Easing.OutQuint);
if (cursorRotate.Value && dragRotationState != DragRotationState.Rotating)
{
// if cursor is already rotating don't reset its rotate origin
dragRotationState = DragRotationState.DragStarted;
positionMouseDown = e.MousePosition;
}
}
return base.OnMouseDown(e);

View File

@ -65,8 +65,10 @@ namespace osu.Game.Graphics
public void SetContent(DateTimeOffset date)
{
dateText.Text = $"{date:d MMMM yyyy} ";
timeText.Text = $"{date:HH:mm:ss \"UTC\"z}";
DateTimeOffset localDate = date.ToLocalTime();
dateText.Text = $"{localDate:d MMMM yyyy} ";
timeText.Text = $"{localDate:HH:mm:ss \"UTC\"z}";
}
public void Move(Vector2 pos) => Position = pos;

View File

@ -1,10 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions;
namespace osu.Game.Graphics.UserInterface
{
public class OsuNumberBox : OsuTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
}
}

View File

@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -120,16 +120,21 @@ namespace osu.Game.Online.Chat
private void checkForMentions(Channel channel, Message message)
{
if (!notifyOnUsername.Value || !checkContainsUsername(message.Content, localUser.Value.Username)) return;
if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return;
notifications.Post(new MentionNotification(message.Sender.Username, channel));
}
/// <summary>
/// Checks if <paramref name="message"/> contains <paramref name="username"/>.
/// Checks if <paramref name="message"/> mentions <paramref name="username"/>.
/// This will match against the case where underscores are used instead of spaces (which is how osu-stable handles usernames with spaces).
/// </summary>
private static bool checkContainsUsername(string message, string username) => message.Contains(username, StringComparison.OrdinalIgnoreCase) || message.Contains(username.Replace(' ', '_'), StringComparison.OrdinalIgnoreCase);
public static bool CheckContainsUsername(string message, string username)
{
string fullName = Regex.Escape(username);
string underscoreName = Regex.Escape(username.Replace(' ', '_'));
return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase);
}
public class PrivateMessageNotification : OpenChannelNotification
{

View File

@ -77,6 +77,11 @@ namespace osu.Game.Online.Multiplayer
/// <exception cref="InvalidStateException">If an attempt to start the game occurs when the game's (or users') state disallows it.</exception>
Task StartMatch();
/// <summary>
/// Aborts an ongoing gameplay load.
/// </summary>
Task AbortGameplay();
/// <summary>
/// Adds an item to the playlist.
/// </summary>

View File

@ -95,8 +95,6 @@ namespace osu.Game.Online.Multiplayer
protected readonly BindableList<int> PlayingUserIds = new BindableList<int>();
public readonly Bindable<PlaylistItem?> CurrentMatchPlayingItem = new Bindable<PlaylistItem?>();
/// <summary>
/// The <see cref="MultiplayerRoomUser"/> corresponding to the local player, if available.
/// </summary>
@ -162,9 +160,6 @@ namespace osu.Game.Online.Multiplayer
var joinedRoom = await JoinRoom(room.RoomID.Value.Value, password ?? room.Password.Value).ConfigureAwait(false);
Debug.Assert(joinedRoom != null);
// Populate playlist items.
var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(item => createPlaylistItem(item, item.ID == joinedRoom.Settings.PlaylistItemId))).ConfigureAwait(false);
// Populate users.
Debug.Assert(joinedRoom.Users != null);
await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false);
@ -176,7 +171,7 @@ namespace osu.Game.Online.Multiplayer
APIRoom = room;
APIRoom.Playlist.Clear();
APIRoom.Playlist.AddRange(playlistItems);
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
Debug.Assert(LocalUser != null);
addUserToAPIRoom(LocalUser);
@ -219,7 +214,6 @@ namespace osu.Game.Online.Multiplayer
{
APIRoom = null;
Room = null;
CurrentMatchPlayingItem.Value = null;
PlayingUserIds.Clear();
RoomUpdated?.Invoke();
@ -333,6 +327,8 @@ namespace osu.Game.Online.Multiplayer
public abstract Task StartMatch();
public abstract Task AbortGameplay();
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
@ -477,28 +473,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Debug.Assert(Room != null);
Scheduler.Add(() =>
{
// ensure the new selected item is populated immediately.
var playlistItem = APIRoom.Playlist.Single(p => p.ID == newSettings.PlaylistItemId);
if (playlistItem != null)
{
GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b =>
{
// Should be called outside of the `Scheduler` logic (and specifically accessing `Exception`) to suppress an exception from firing outwards.
bool success = b.Exception == null;
Scheduler.Add(() =>
{
if (success)
playlistItem.Beatmap.Value = b.Result;
updateLocalRoomSettings(newSettings);
});
});
}
});
Scheduler.Add(() => updateLocalRoomSettings(newSettings));
return Task.CompletedTask;
}
@ -653,12 +628,10 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
public async Task PlaylistItemAdded(MultiplayerPlaylistItem item)
public Task PlaylistItemAdded(MultiplayerPlaylistItem item)
{
if (Room == null)
return;
var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
return Task.CompletedTask;
Scheduler.Add(() =>
{
@ -668,11 +641,13 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(APIRoom != null);
Room.Playlist.Add(item);
APIRoom.Playlist.Add(playlistItem);
APIRoom.Playlist.Add(createPlaylistItem(item));
ItemAdded?.Invoke(item);
RoomUpdated?.Invoke();
});
return Task.CompletedTask;
}
public Task PlaylistItemRemoved(long playlistItemId)
@ -697,12 +672,10 @@ namespace osu.Game.Online.Multiplayer
return Task.CompletedTask;
}
public async Task PlaylistItemChanged(MultiplayerPlaylistItem item)
public Task PlaylistItemChanged(MultiplayerPlaylistItem item)
{
if (Room == null)
return;
var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
return Task.CompletedTask;
Scheduler.Add(() =>
{
@ -715,15 +688,13 @@ namespace osu.Game.Online.Multiplayer
int existingIndex = APIRoom.Playlist.IndexOf(APIRoom.Playlist.Single(existing => existing.ID == item.ID));
APIRoom.Playlist.RemoveAt(existingIndex);
APIRoom.Playlist.Insert(existingIndex, playlistItem);
// If the currently-selected item was the one that got replaced, update the selected item to the new one.
if (CurrentMatchPlayingItem.Value?.ID == playlistItem.ID)
CurrentMatchPlayingItem.Value = playlistItem;
APIRoom.Playlist.Insert(existingIndex, createPlaylistItem(item));
ItemChanged?.Invoke(item);
RoomUpdated?.Invoke();
});
return Task.CompletedTask;
}
/// <summary>
@ -752,12 +723,11 @@ namespace osu.Game.Online.Multiplayer
APIRoom.Password.Value = Room.Settings.Password;
APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
RoomUpdated?.Invoke();
CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId);
RoomUpdated?.Invoke();
}
private async Task<PlaylistItem> createPlaylistItem(MultiplayerPlaylistItem item, bool populateBeatmapImmediately)
private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item)
{
var ruleset = Rulesets.GetRuleset(item.RulesetID);
@ -779,9 +749,6 @@ namespace osu.Game.Online.Multiplayer
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance)));
if (populateBeatmapImmediately)
playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false);
return playlistItem;
}
@ -791,7 +758,7 @@ namespace osu.Game.Online.Multiplayer
/// <param name="beatmapId">The beatmap ID.</param>
/// <param name="cancellationToken">A token to cancel the request.</param>
/// <returns>The <see cref="APIBeatmap"/> retrieval task.</returns>
protected abstract Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default);
public abstract Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default);
/// <summary>
/// For the provided user ID, update whether the user is included in <see cref="CurrentMatchPlayingUserIds"/>.

View File

@ -154,6 +154,14 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch));
}
public override Task AbortGameplay()
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.AbortGameplay));
}
public override Task AddPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)
@ -178,7 +186,7 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
}
protected override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
public override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{
return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken);
}

View File

@ -40,6 +40,11 @@ namespace osu.Game.Online.Rooms
private BeatmapDownloadTracker downloadTracker;
/// <summary>
/// The beatmap matching the required hash (and providing a final <see cref="BeatmapAvailability.LocallyAvailable"/> state).
/// </summary>
private BeatmapInfo matchingHash;
protected override void LoadComplete()
{
base.LoadComplete();
@ -71,13 +76,34 @@ namespace osu.Game.Online.Rooms
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
}, true);
}, true);
// These events are needed for a fringe case where a modified/altered beatmap is imported with matching OnlineIDs.
// During the import process this will cause the existing beatmap set to be silently deleted and replaced with the new one.
// This is not exposed to us via `BeatmapDownloadTracker` so we have to take it into our own hands (as we care about the hash matching).
beatmapManager.ItemUpdated += itemUpdated;
beatmapManager.ItemRemoved += itemRemoved;
}
private void itemUpdated(BeatmapSetInfo item) => Schedule(() =>
{
if (matchingHash?.BeatmapSet.ID == item.ID || SelectedItem.Value?.Beatmap.Value.BeatmapSet?.OnlineID == item.OnlineID)
updateAvailability();
});
private void itemRemoved(BeatmapSetInfo item) => Schedule(() =>
{
if (matchingHash?.BeatmapSet.ID == item.ID)
updateAvailability();
});
private void updateAvailability()
{
if (downloadTracker == null)
return;
// will be repopulated below if still valid.
matchingHash = null;
switch (downloadTracker.State.Value)
{
case DownloadState.NotDownloaded:
@ -93,7 +119,9 @@ namespace osu.Game.Online.Rooms
break;
case DownloadState.LocallyAvailable:
bool hashMatches = checkHashValidity();
matchingHash = findMatchingHash();
bool hashMatches = matchingHash != null;
availability.Value = hashMatches ? BeatmapAvailability.LocallyAvailable() : BeatmapAvailability.NotDownloaded();
@ -108,12 +136,23 @@ namespace osu.Game.Online.Rooms
}
}
private bool checkHashValidity()
private BeatmapInfo findMatchingHash()
{
int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending) != null;
return beatmapManager.QueryBeatmap(b => b.OnlineID == onlineId && b.MD5Hash == checksum && !b.BeatmapSet.DeletePending);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmapManager != null)
{
beatmapManager.ItemUpdated -= itemUpdated;
beatmapManager.ItemRemoved -= itemRemoved;
}
}
}
}

View File

@ -237,10 +237,7 @@ namespace osu.Game.Overlays
Schedule(() =>
{
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged;
foreach (Channel channel in channelManager.JoinedChannels)
ChannelTabControl.AddChannel(channel);
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged;
availableChannelsChanged(null, null);
@ -436,12 +433,19 @@ namespace osu.Game.Overlays
{
case NotifyCollectionChangedAction.Add:
foreach (Channel channel in args.NewItems.Cast<Channel>())
ChannelTabControl.AddChannel(channel);
{
if (channel.Type != ChannelType.Multiplayer)
ChannelTabControl.AddChannel(channel);
}
break;
case NotifyCollectionChangedAction.Remove:
foreach (Channel channel in args.OldItems.Cast<Channel>())
{
if (!ChannelTabControl.Items.Contains(channel))
continue;
ChannelTabControl.RemoveChannel(channel);
var loaded = loadedChannels.Find(c => c.Channel == channel);

View File

@ -5,12 +5,14 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
@ -28,6 +30,8 @@ namespace osu.Game.Overlays.OSD
private Sample sampleOff;
private Sample sampleChange;
private Bindable<double?> lastPlaybackTime;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
@ -75,10 +79,28 @@ namespace osu.Game.Overlays.OSD
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
[Resolved]
private SessionStatics statics { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
playSound();
}
private void playSound()
{
// This debounce code roughly follows what we're using in HoverSampleDebounceComponent.
// We're sharing the existing static for hover sounds because it doesn't really matter if they block each other.
// This is a simple solution, but if this ever becomes a problem (or other performance issues arise),
// the whole toast system should be rewritten to avoid recreating this drawable each time a value changes.
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
if (!enoughTimePassedSinceLastPlayback) return;
if (optionCount == 1)
{
if (selectedOption == 0)
@ -93,6 +115,8 @@ namespace osu.Game.Overlays.OSD
sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f;
sampleChange.Play();
}
lastPlaybackTime.Value = Time.Current;
}
[BackgroundDependencyLoader]

View File

@ -101,7 +101,7 @@ namespace osu.Game.Overlays
DisplayTemporarily(box);
});
private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description));
private void displayTrackedSettingChange(SettingDescription description) => Scheduler.AddOnce(Display, new TrackedSettingToast(description));
private TransformSequence<Drawable> fadeIn;
private ScheduledDelegate fadeOut;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@ -67,7 +68,7 @@ namespace osu.Game.Overlays.Settings
private class OutlinedNumberBox : OutlinedTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
protected override bool CanAddCharacter(char character) => character.IsAsciiDigit();
public new void NotifyInputError() => base.NotifyInputError();
}

View File

@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mods
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
// AlwaysPresent required for hitsounds
drawableRuleset.Playfield.AlwaysPresent = true;
drawableRuleset.Playfield.Hide();
drawableRuleset.AlwaysPresent = true;
drawableRuleset.Hide();
}
}

View File

@ -48,16 +48,19 @@ namespace osu.Game.Screens.Backgrounds
AddInternal(seasonalBackgroundLoader);
user.ValueChanged += _ => Next();
skin.ValueChanged += _ => Next();
mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next();
seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Next();
user.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
skin.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
mode.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
beatmap.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
introSequence.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(loadNextIfRequired);
currentDisplay = RNG.Next(0, background_count);
Next();
// helper function required for AddOnce usage.
void loadNextIfRequired() => Next();
}
private ScheduledDelegate nextTask;
@ -67,7 +70,7 @@ namespace osu.Game.Screens.Backgrounds
/// Request loading the next background.
/// </summary>
/// <returns>Whether a new background was queued for load. May return false if the current background is still valid.</returns>
public bool Next()
public virtual bool Next()
{
var nextBackground = createBackground();

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -24,6 +25,7 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Rulesets;
@ -90,6 +92,10 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private UserLookupCache userLookupCache { get; set; }
[CanBeNull]
[Resolved(CanBeNull = true)]
private MultiplayerClient multiplayerClient { get; set; }
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
@ -157,7 +163,15 @@ namespace osu.Game.Screens.OnlinePlay
if (Item.Beatmap.Value == null)
{
var foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false);
IBeatmapInfo foundBeatmap;
if (multiplayerClient != null)
// This call can eventually go away (and use the else case below).
// Currently required only due to the method being overridden to provide special behaviour in tests.
foundBeatmap = await multiplayerClient.GetAPIBeatmap(Item.BeatmapID).ConfigureAwait(false);
else
foundBeatmap = await beatmapLookupCache.GetBeatmapAsync(Item.BeatmapID).ConfigureAwait(false);
Schedule(() => Item.Beatmap.Value = foundBeatmap);
}
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner
{
[Cached(typeof(IBindable<PlaylistItem>))]
protected readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();
public override bool? AllowTrackAdjustments => true;
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Match
protected OnlinePlayScreen ParentScreen { get; private set; }
[Cached]
private OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker { get; set; }
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
protected IBindable<BeatmapAvailability> BeatmapAvailability => beatmapAvailabilityTracker.Availability;
@ -90,11 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
Padding = new MarginPadding { Top = Header.HEIGHT };
beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = SelectedItem }
};
RoomId.BindTo(room.RoomID);
}
@ -247,10 +242,10 @@ namespace osu.Game.Screens.OnlinePlay.Match
}, true);
SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged));
beatmapManager.ItemUpdated += beatmapUpdated;
UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods));
beatmapAvailabilityTracker.SelectedItem.BindTo(SelectedItem);
beatmapAvailabilityTracker.Availability.BindValueChanged(_ => updateWorkingBeatmap());
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -374,8 +369,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
}
}
private void beatmapUpdated(BeatmapSetInfo set) => Schedule(updateWorkingBeatmap);
private void updateWorkingBeatmap()
{
var beatmap = SelectedItem.Value?.Beatmap.Value;
@ -443,14 +436,6 @@ namespace osu.Game.Screens.OnlinePlay.Match
/// <param name="room">The room to change the settings of.</param>
protected abstract RoomSettingsOverlay CreateRoomSettingsOverlay(Room room);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (beatmapManager != null)
beatmapManager.ItemUpdated -= beatmapUpdated;
}
public class UserModSelectButton : PurpleTriangleButton
{
}

View File

@ -63,6 +63,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
}
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(_ => updateState());
}
protected override void OnRoomUpdated()
{
base.OnRoomUpdated();
@ -104,7 +111,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
bool enableButton =
Room?.State == MultiplayerRoomState.Open
&& Client.CurrentMatchPlayingItem.Value?.Expired == false
&& SelectedItem.Value?.ID == Room.Settings.PlaylistItemId
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
&& !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.

View File

@ -121,7 +121,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
private void addItemToLists(MultiplayerPlaylistItem item)
{
var apiItem = Playlist.Single(i => i.ID == item.ID);
var apiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
// Item could have been removed from the playlist while the local player was in gameplay.
if (apiItem == null)
return;
if (item.Expired)
historyList.Items.Add(apiItem);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Multiplayer;
@ -18,8 +19,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.OnResuming(last);
if (client.Room != null && client.LocalUser?.State != MultiplayerUserState.Spectating)
client.ChangeState(MultiplayerUserState.Idle);
if (client.Room == null)
return;
Debug.Assert(client.LocalUser != null);
switch (client.LocalUser.State)
{
case MultiplayerUserState.Spectating:
break;
case MultiplayerUserState.WaitingForLoad:
case MultiplayerUserState.Loaded:
case MultiplayerUserState.Playing:
client.AbortGameplay();
break;
default:
client.ChangeState(MultiplayerUserState.Idle);
break;
}
}
protected override string ScreenTitle => "Multiplayer";

View File

@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private MultiplayerClient client { get; set; }
private readonly PlaylistItem itemToEdit;
private readonly long? itemToEdit;
private LoadingLayer loadingLayer;
@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
/// <param name="itemToEdit">The item to be edited. May be null, in which case a new item will be added to the playlist.</param>
/// <param name="beatmap">An optional initial beatmap selection to perform.</param>
/// <param name="ruleset">An optional initial ruleset selection to perform.</param>
public MultiplayerMatchSongSelect(Room room, PlaylistItem itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
public MultiplayerMatchSongSelect(Room room, long? itemToEdit = null, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room)
{
this.itemToEdit = itemToEdit;
@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
var multiplayerItem = new MultiplayerPlaylistItem
{
ID = itemToEdit?.ID ?? 0,
ID = itemToEdit ?? 0,
BeatmapID = item.BeatmapID,
BeatmapChecksum = item.Beatmap.Value.MD5Hash,
RulesetID = item.RulesetID,

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -67,8 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
base.LoadComplete();
SelectedItem.BindTo(client.CurrentMatchPlayingItem);
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
@ -147,7 +146,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
new MultiplayerPlaylist
{
RelativeSizeAxes = Axes.Both,
RequestEdit = OpenSongSelection
RequestEdit = item => OpenSongSelection(item.ID)
}
},
new[]
@ -224,7 +223,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
/// Opens the song selection screen to add or edit an item.
/// </summary>
/// <param name="itemToEdit">An optional playlist item to edit. If null, a new item will be added instead.</param>
internal void OpenSongSelection([CanBeNull] PlaylistItem itemToEdit = null)
internal void OpenSongSelection(long? itemToEdit = null)
{
if (!this.IsCurrentScreen())
return;
@ -327,10 +326,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
else
else if (client.LocalUser?.State == MultiplayerUserState.Spectating
&& (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
{
if (client.LocalUser?.State == MultiplayerUserState.Spectating && (client.Room?.State == MultiplayerRoomState.WaitingForLoad || client.Room?.State == MultiplayerRoomState.Playing))
onLoadRequested();
onLoadRequested();
}
}
@ -389,11 +388,50 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
updateCurrentItem();
addItemButton.Alpha = client.IsHost || Room.QueueMode.Value != QueueMode.HostOnly ? 1 : 0;
Scheduler.AddOnce(UpdateMods);
}
private void updateCurrentItem()
{
Debug.Assert(client.Room != null);
var expectedSelectedItem = Room.Playlist.SingleOrDefault(i => i.ID == client.Room.Settings.PlaylistItemId);
if (expectedSelectedItem == null)
return;
// There's no reason to renew the selected item if its content hasn't changed.
if (SelectedItem.Value?.Equals(expectedSelectedItem) == true && expectedSelectedItem.Beatmap.Value != null)
return;
// Clear the selected item while the lookup is performed, so components like the ready button can enter their disabled states.
SelectedItem.Value = null;
if (expectedSelectedItem.Beatmap.Value == null)
{
Task.Run(async () =>
{
var beatmap = await client.GetAPIBeatmap(expectedSelectedItem.BeatmapID).ConfigureAwait(false);
Schedule(() =>
{
expectedSelectedItem.Beatmap.Value = beatmap;
if (Room.Playlist.SingleOrDefault(i => i.ID == client.Room?.Settings.PlaylistItemId)?.Equals(expectedSelectedItem) == true)
applyCurrentItem();
});
});
}
else
applyCurrentItem();
void applyCurrentItem() => SelectedItem.Value = expectedSelectedItem;
}
private void handleRoomLost() => Schedule(() =>
{
if (this.IsCurrentScreen())
@ -446,6 +484,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!this.IsCurrentScreen())
return;
if (client.Room == null)
return;
if (!client.IsHost)
{
// todo: should handle this when the request queue is implemented.
@ -454,7 +495,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
return;
}
this.Push(new MultiplayerMatchSongSelect(Room, SelectedItem.Value, beatmap, ruleset));
this.Push(new MultiplayerMatchSongSelect(Room, client.Room.Settings.PlaylistItemId, beatmap, ruleset));
}
protected override void Dispose(bool isDisposing)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -226,8 +227,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public override bool OnBackButton()
{
// On a manual exit, set the player state back to idle.
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
Debug.Assert(multiplayerClient.Room != null);
// On a manual exit, set the player back to idle unless gameplay has finished.
if (multiplayerClient.Room.State != MultiplayerRoomState.Open)
multiplayerClient.ChangeState(MultiplayerUserState.Idle);
return base.OnBackButton();
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Game.Rulesets.UI;
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using ManagedBass.Fx;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample;
@ -18,6 +19,7 @@ using osu.Framework.Utils;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
@ -58,6 +60,12 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both,
};
/// <summary>
/// The player screen background, used to adjust appearance on failing.
/// </summary>
[CanBeNull]
public BackgroundScreen Background { private get; set; }
public FailAnimation(DrawableRuleset drawableRuleset)
{
this.drawableRuleset = drawableRuleset;
@ -136,6 +144,9 @@ namespace osu.Game.Screens.Play
Content.ScaleTo(0.85f, duration, Easing.OutQuart);
Content.RotateTo(1, duration, Easing.OutQuart);
Content.FadeColour(Color4.Gray, duration);
// Will be restored by `ApplyToBackground` logic in `SongSelect`.
Background?.FadeColour(OsuColour.Gray(0.3f), 60);
}
public void RemoveFilters(bool resetTrackFrequency = true)

View File

@ -921,6 +921,8 @@ namespace osu.Game.Screens.Play
b.IsBreakTime.BindTo(breakTracker.IsBreakTime);
b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground);
failAnimationLayer.Background = b;
});
HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime);

View File

@ -228,10 +228,7 @@ namespace osu.Game.Screens.Play
onlineBeatmapRequest.Success += beatmapSet => Schedule(() =>
{
this.beatmapSet = beatmapSet;
beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet)
{
Expanded = { Disabled = true }
};
beatmapPanelContainer.Child = new BeatmapCard(this.beatmapSet, allowExpansion: false);
checkForAutomaticDownload();
});

View File

@ -33,10 +33,8 @@ namespace osu.Game.Storyboards
foreach (var l in loops)
{
if (!(l.EarliestDisplayedTime is double lEarliest))
continue;
earliestStartTime = Math.Min(earliestStartTime, lEarliest);
if (l.EarliestDisplayedTime is double loopEarliestDisplayTime)
earliestStartTime = Math.Min(earliestStartTime, l.LoopStartTime + loopEarliestDisplayTime);
}
if (earliestStartTime < double.MaxValue)

View File

@ -128,6 +128,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
case MultiplayerRoomState.WaitingForLoad:
if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad))
{
var loadedUsers = Room.Users.Where(u => u.State == MultiplayerUserState.Loaded).ToArray();
if (loadedUsers.Length == 0)
{
// all users have bailed from the load sequence. cancel the game start.
ChangeRoomState(MultiplayerRoomState.Open);
return;
}
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded))
ChangeUserState(u.UserID, MultiplayerUserState.Playing);
@ -143,8 +152,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay))
ChangeUserState(u.UserID, MultiplayerUserState.Results);
ChangeRoomState(MultiplayerRoomState.Open);
ChangeRoomState(MultiplayerRoomState.Open);
((IMultiplayerClient)this).ResultsReady();
FinishCurrentItem().Wait();
@ -242,6 +251,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task ChangeState(MultiplayerUserState newState)
{
Debug.Assert(Room != null);
if (newState == MultiplayerUserState.Idle && LocalUser?.State == MultiplayerUserState.WaitingForLoad)
return Task.CompletedTask;
ChangeUserState(api.LocalUser.Value.Id, newState);
return Task.CompletedTask;
}
@ -303,6 +317,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
return ((IMultiplayerClient)this).LoadRequested();
}
public override Task AbortGameplay()
{
Debug.Assert(Room != null);
Debug.Assert(LocalUser != null);
ChangeUserState(LocalUser.UserID, MultiplayerUserState.Idle);
return Task.CompletedTask;
}
public async Task AddUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{
Debug.Assert(Room != null);
@ -376,7 +400,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, playlistItemId);
protected override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
public override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{
IBeatmapSetInfo? set = roomManager.ServerSideRooms.SelectMany(r => r.Playlist)
.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.7.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1210.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1215.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
<PackageReference Include="Sentry" Version="3.12.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -60,8 +60,8 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1210.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.1215.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1215.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>
@ -83,7 +83,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1210.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.1215.0" />
<PackageReference Include="SharpCompress" Version="0.30.0" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />