mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 15:47:26 +08:00
Merge branch 'master' into multiplayer-auto-countdown
This commit is contained in:
commit
40eca0fbe2
@ -27,7 +27,7 @@
|
||||
]
|
||||
},
|
||||
"ppy.localisationanalyser.tools": {
|
||||
"version": "2021.1210.0",
|
||||
"version": "2022.320.0",
|
||||
"commands": [
|
||||
"localisation"
|
||||
]
|
||||
|
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
72
.github/ISSUE_TEMPLATE/bug-issue.yml
vendored
Normal file
@ -0,0 +1,72 @@
|
||||
name: Bug report
|
||||
description: Report a very clearly broken issue.
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
# osu! bug report
|
||||
|
||||
Important to note that your issue may have already been reported before. Please check:
|
||||
- Pinned issues, at the top of https://github.com/ppy/osu/issues.
|
||||
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
|
||||
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful.
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Type
|
||||
options:
|
||||
- Crash to desktop
|
||||
- Game behaviour
|
||||
- Performance
|
||||
- Cosmetic
|
||||
- Other
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Bug description
|
||||
description: How did you find the bug? Any additional details that might help?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Screenshots or videos
|
||||
description: Add screenshots or videos that show the bug here.
|
||||
placeholder: Drag and drop the screenshots/videos into this box.
|
||||
validations:
|
||||
required: false
|
||||
- type: input
|
||||
attributes:
|
||||
label: Version
|
||||
description: The version you encountered this bug on. This is shown at the bottom of the main menu and also at the end of the settings screen.
|
||||
validations:
|
||||
required: true
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Logs
|
||||
|
||||
Attaching log files is required for every reported bug. See instructions below on how to find them.
|
||||
|
||||
If the game has not yet been closed since you found the bug:
|
||||
1. Head on to game settings and click on "Open osu! folder"
|
||||
2. Then open the `logs` folder located there
|
||||
|
||||
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
|
||||
|
||||
The default places to find the logs are as follows:
|
||||
- `%AppData%/osu/logs` *on Windows*
|
||||
- `~/.local/share/osu/logs` *on Linux & macOS*
|
||||
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
|
||||
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
|
||||
|
||||
If you have selected a custom location for the game files, you can find the `logs` folder there.
|
||||
|
||||
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Logs
|
||||
placeholder: Drag and drop the log files into this box.
|
||||
validations:
|
||||
required: true
|
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp"
|
||||
]
|
||||
}
|
@ -31,7 +31,7 @@ If you are looking to install or test osu! without setting up a development envi
|
||||
|
||||
**Latest build:**
|
||||
|
||||
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.15+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
|
||||
| [Windows 8.1+ (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | macOS 10.15+ ([Intel](https://github.com/ppy/osu/releases/latest/download/osu.app.Intel.zip), [Apple Silicon](https://github.com/ppy/osu/releases/latest/download/osu.app.Apple.Silicon.zip)) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
|
||||
| ------------- | ------------- | ------------- | ------------- | ------------- |
|
||||
|
||||
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
|
||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override string Description => "No need to chase the circle – the circle chases you!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
|
||||
|
||||
private IFrameStableClock gameplayClock;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
|
||||
|
||||
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||
|
||||
|
@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
|
||||
{
|
||||
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// How early before a hitobject's start time to trigger a hit.
|
||||
|
148
osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
Normal file
148
osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.UI;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
|
||||
{
|
||||
public override string Name => @"Strict Tracking";
|
||||
public override string Acronym => @"ST";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => @"Follow circles just got serious...";
|
||||
public override double ScoreMultiplier => 1.0;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
|
||||
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
if (drawable is DrawableSlider slider)
|
||||
{
|
||||
slider.Tracking.ValueChanged += e =>
|
||||
{
|
||||
if (e.NewValue || slider.Judged) return;
|
||||
|
||||
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
|
||||
|
||||
if (!tail.Judged)
|
||||
tail.MissForcefully();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
var osuBeatmap = (OsuBeatmap)beatmap;
|
||||
|
||||
if (osuBeatmap.HitObjects.Count == 0) return;
|
||||
|
||||
var hitObjects = osuBeatmap.HitObjects.Select(ho =>
|
||||
{
|
||||
if (ho is Slider slider)
|
||||
{
|
||||
var newSlider = new StrictTrackingSlider(slider);
|
||||
return newSlider;
|
||||
}
|
||||
|
||||
return ho;
|
||||
}).ToList();
|
||||
|
||||
osuBeatmap.HitObjects = hitObjects;
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.Playfield.RegisterPool<StrictTrackingSliderTailCircle, StrictTrackingDrawableSliderTail>(10, 100);
|
||||
}
|
||||
|
||||
private class StrictTrackingSliderTailCircle : SliderTailCircle
|
||||
{
|
||||
public StrictTrackingSliderTailCircle(Slider slider)
|
||||
: base(slider)
|
||||
{
|
||||
}
|
||||
|
||||
public override Judgement CreateJudgement() => new OsuJudgement();
|
||||
}
|
||||
|
||||
private class StrictTrackingDrawableSliderTail : DrawableSliderTail
|
||||
{
|
||||
public override bool DisplayResult => true;
|
||||
}
|
||||
|
||||
private class StrictTrackingSlider : Slider
|
||||
{
|
||||
public StrictTrackingSlider(Slider original)
|
||||
{
|
||||
StartTime = original.StartTime;
|
||||
Samples = original.Samples;
|
||||
Path = original.Path;
|
||||
NodeSamples = original.NodeSamples;
|
||||
RepeatCount = original.RepeatCount;
|
||||
Position = original.Position;
|
||||
NewCombo = original.NewCombo;
|
||||
ComboOffset = original.ComboOffset;
|
||||
LegacyLastTickOffset = original.LegacyLastTickOffset;
|
||||
TickDistanceMultiplier = original.TickDistanceMultiplier;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
|
||||
|
||||
foreach (var e in sliderEvents)
|
||||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
case SliderEventType.Head:
|
||||
AddNested(HeadCircle = new SliderHeadCircle
|
||||
{
|
||||
StartTime = e.Time,
|
||||
Position = Position,
|
||||
StackHeight = StackHeight,
|
||||
});
|
||||
break;
|
||||
|
||||
case SliderEventType.LegacyLastTick:
|
||||
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = e.Time,
|
||||
Position = EndPosition,
|
||||
StackHeight = StackHeight
|
||||
});
|
||||
break;
|
||||
|
||||
case SliderEventType.Repeat:
|
||||
AddNested(new SliderRepeat(this)
|
||||
{
|
||||
RepeatIndex = e.SpanIndex,
|
||||
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
|
||||
Position = Position + Path.PositionAt(e.PathProgress),
|
||||
StackHeight = StackHeight,
|
||||
Scale = Scale,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateNestedSamples();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
|
||||
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
}
|
||||
}
|
||||
|
||||
updateNestedSamples();
|
||||
UpdateNestedSamples();
|
||||
}
|
||||
|
||||
private void updateNestedPositions()
|
||||
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
TailCircle.Position = EndPosition;
|
||||
}
|
||||
|
||||
private void updateNestedSamples()
|
||||
protected void UpdateNestedSamples()
|
||||
{
|
||||
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
|
||||
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)
|
||||
|
@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
|
||||
new OsuModHidden(),
|
||||
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
|
||||
new OsuModStrictTracking()
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (osuComponent.Component)
|
||||
{
|
||||
case OsuSkinComponents.FollowPoint:
|
||||
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false);
|
||||
return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
|
||||
|
||||
case OsuSkinComponents.SliderFollowCircle:
|
||||
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||
|
@ -590,6 +590,8 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.IsTrue(imported.DeletePending);
|
||||
|
||||
var originalAddedDate = imported.DateAdded;
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
@ -597,6 +599,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsFalse(imported.DeletePending);
|
||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
|
||||
});
|
||||
}
|
||||
|
||||
@ -646,6 +649,8 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.IsTrue(imported.DeletePending);
|
||||
|
||||
var originalAddedDate = imported.DateAdded;
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||
@ -653,6 +658,7 @@ namespace osu.Game.Tests.Database
|
||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||
Assert.IsFalse(imported.DeletePending);
|
||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -23,6 +24,7 @@ using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Zip;
|
||||
|
||||
@ -63,13 +65,19 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
EditorBeatmap editorBeatmap = null;
|
||||
|
||||
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
|
||||
AddStep("exit without save", () =>
|
||||
|
||||
AddStep("exit without save", () => Editor.Exit());
|
||||
AddStep("hold to confirm", () =>
|
||||
{
|
||||
Editor.Exit();
|
||||
DialogOverlay.CurrentDialog.PerformOkAction();
|
||||
var confirmButton = DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogDangerousButton>().First();
|
||||
|
||||
InputManager.MoveMouseTo(confirmButton);
|
||||
InputManager.PressButton(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,13 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public class TestSceneEditorSaving : EditorSavingTestScene
|
||||
{
|
||||
[Test]
|
||||
public void TestCantExitWithoutSaving()
|
||||
{
|
||||
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
|
||||
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMetadata()
|
||||
{
|
||||
|
@ -1,14 +1,19 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -29,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("reload skin editor", () =>
|
||||
{
|
||||
skinEditor?.Expire();
|
||||
Player.ScaleTo(0.8f);
|
||||
Player.ScaleTo(0.4f);
|
||||
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
|
||||
});
|
||||
}
|
||||
@ -40,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditComponent()
|
||||
{
|
||||
BarHitErrorMeter hitErrorMeter = null;
|
||||
|
||||
AddStep("select bar hit error blueprint", () =>
|
||||
{
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
|
||||
|
||||
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
});
|
||||
|
||||
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
|
||||
|
||||
AddStep("hover first slider", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(
|
||||
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
|
||||
.ChildrenOfType<SettingsSlider<float>>().First()
|
||||
.ChildrenOfType<SliderBar<float>>().First()
|
||||
);
|
||||
});
|
||||
|
||||
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
|
||||
|
||||
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
|
||||
}
|
||||
|
||||
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,56 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSeekToGameplayStartFramesArriveAfterPlayerLoad()
|
||||
{
|
||||
const double gameplay_start = 10000;
|
||||
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
|
||||
waitForPlayer();
|
||||
|
||||
sendFrames(startTime: gameplay_start);
|
||||
|
||||
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests the same as <see cref="TestSeekToGameplayStartFramesArriveAfterPlayerLoad"/> but with the frames arriving just as <see cref="Player"/> is transitioning into existence.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestSeekToGameplayStartFramesArriveAsPlayerLoaded()
|
||||
{
|
||||
const double gameplay_start = 10000;
|
||||
|
||||
loadSpectatingScreen();
|
||||
|
||||
start();
|
||||
|
||||
AddUntilStep("wait for player loader", () => (Stack.CurrentScreen as PlayerLoader)?.IsLoaded == true);
|
||||
|
||||
AddUntilStep("queue send frames on player load", () =>
|
||||
{
|
||||
var loadingPlayer = (Stack.CurrentScreen as PlayerLoader)?.CurrentPlayer;
|
||||
|
||||
if (loadingPlayer == null)
|
||||
return false;
|
||||
|
||||
loadingPlayer.OnLoadComplete += _ =>
|
||||
{
|
||||
spectatorClient.SendFramesFromUser(streamingUser.Id, 10, gameplay_start);
|
||||
};
|
||||
return true;
|
||||
});
|
||||
|
||||
waitForPlayer();
|
||||
|
||||
AddUntilStep("state is playing", () => spectatorClient.WatchedUserStates[streamingUser.Id].State == SpectatedUserState.Playing);
|
||||
AddAssert("time is greater than seek target", () => currentFrameStableTime > gameplay_start);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFrameStarvationAndResume()
|
||||
{
|
||||
@ -319,9 +369,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private void checkPaused(bool state) =>
|
||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == state);
|
||||
|
||||
private void sendFrames(int count = 10)
|
||||
private void sendFrames(int count = 10, double startTime = 0)
|
||||
{
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count));
|
||||
AddStep("send frames", () => spectatorClient.SendFramesFromUser(streamingUser.Id, count, startTime));
|
||||
}
|
||||
|
||||
private void loadSpectatingScreen()
|
||||
|
@ -95,10 +95,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
protected void RunGameplay()
|
||||
{
|
||||
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
|
||||
AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
|
||||
|
@ -102,10 +102,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
|
||||
|
||||
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
|
||||
AddAssert("ruleset is correct", () => ((Player)CurrentScreen).Ruleset.Value.Equals(new OsuRuleset().RulesetInfo));
|
||||
@ -119,10 +119,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("selected beatmap is initial beatmap", () => Beatmap.Value.BeatmapInfo.OnlineID == InitialBeatmap.OnlineID);
|
||||
|
||||
AddUntilStep("wait for idle", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Idle);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("wait for player", () => CurrentScreen is Player player && player.IsLoaded);
|
||||
AddAssert("mods are correct", () => !((Player)CurrentScreen).Mods.Value.Any());
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@ -27,9 +28,9 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
|
||||
public class TestSceneMatchStartControl : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerReadyButton button;
|
||||
private MatchStartControl control;
|
||||
private BeatmapSetInfo importedSet;
|
||||
|
||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||
@ -62,7 +63,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = button = new MultiplayerReadyButton
|
||||
Child = control = new MatchStartControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -74,37 +75,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestStartWithCountdown()
|
||||
{
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
|
||||
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
|
||||
AddAssert("countdown button not visible", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||
AddUntilStep("match started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCancelCountdown()
|
||||
{
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
|
||||
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
}
|
||||
|
||||
@ -117,12 +118,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.TransferHost(2);
|
||||
});
|
||||
|
||||
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(2) }));
|
||||
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
@ -132,25 +133,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().IsPresent);
|
||||
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
|
||||
AddAssert("countdown button is visible", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().IsPresent);
|
||||
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
|
||||
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
|
||||
AddAssert("countdown button disabled", () => !this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
|
||||
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
|
||||
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().Single().Enabled.Value);
|
||||
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
|
||||
{
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
@ -158,7 +159,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
AddStep("finish countdown", () => MultiplayerClient.FinishCountDown());
|
||||
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
|
||||
}
|
||||
|
||||
@ -168,12 +169,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("add second user", () => MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" }));
|
||||
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerReadyButton.CountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.CountdownButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||
AddStep("click the first countdown button", () =>
|
||||
{
|
||||
var popoverButton = this.ChildrenOfType<MultiplayerReadyButton.CountdownButton.PopoverButton>().First();
|
||||
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||
InputManager.MoveMouseTo(popoverButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
@ -181,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton.ReadyButton>().Single().Enabled.Value);
|
||||
AddAssert("ready button enabled", () => this.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -193,13 +194,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.TransferHost(2);
|
||||
});
|
||||
|
||||
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Delay = TimeSpan.FromMinutes(1) }));
|
||||
AddStep("start countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(1) }).WaitSafely());
|
||||
AddUntilStep("countdown started", () => MultiplayerClient.Room?.Countdown != null);
|
||||
|
||||
AddStep("transfer host to local user", () => MultiplayerClient.TransferHost(API.LocalUser.Value.OnlineID));
|
||||
AddUntilStep("local user is host", () => MultiplayerClient.Room?.Host?.Equals(MultiplayerClient.LocalUser) == true);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
|
||||
}
|
||||
@ -211,7 +212,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("ensure ready button enabled", () =>
|
||||
{
|
||||
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
||||
readyButton = control.ChildrenOfType<OsuButton>().Single();
|
||||
return readyButton.Enabled.Value;
|
||||
});
|
||||
|
||||
@ -230,10 +231,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.TransferHost(2);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
}
|
||||
|
||||
@ -249,7 +250,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
|
||||
verifyGameplayStartFlow();
|
||||
@ -264,7 +265,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.TransferHost(2);
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
||||
|
||||
verifyGameplayStartFlow();
|
||||
@ -279,14 +280,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||
});
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
|
||||
AddStep("transfer host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[1].UserID ?? 0));
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
@ -304,7 +305,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
if (!isHost)
|
||||
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddRepeatStep("change user ready state", () =>
|
||||
{
|
||||
@ -322,7 +323,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private void verifyGameplayStartFlow()
|
||||
{
|
||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
||||
|
||||
AddStep("finish gameplay", () =>
|
||||
@ -331,7 +332,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
||||
});
|
||||
|
||||
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||
}
|
||||
}
|
||||
}
|
@ -41,6 +41,7 @@ using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Spectate;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK.Input;
|
||||
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
@ -424,7 +425,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("Beatmap doesn't match current item", () => Beatmap.Value.BeatmapInfo.OnlineID != multiplayerClient.Room?.Playlist.First().BeatmapID);
|
||||
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||
|
||||
@ -462,7 +463,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("Ruleset doesn't match current item", () => Ruleset.Value.OnlineID != multiplayerClient.Room?.Playlist.First().RulesetID);
|
||||
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||
|
||||
@ -500,7 +501,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddAssert("Mods don't match current item", () => !SelectedMods.Value.Select(m => m.Acronym).SequenceEqual(multiplayerClient.Room.AsNonNull().Playlist.First().RequiredMods.Select(m => m.Acronym)));
|
||||
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddUntilStep("play started", () => multiplayerComponents.CurrentScreen is Player);
|
||||
|
||||
@ -535,7 +536,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddAssert("play not started", () => multiplayerComponents.IsCurrentScreen());
|
||||
}
|
||||
@ -568,7 +569,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("wait for spectating user state", () => multiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch());
|
||||
AddStep("start match externally", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddStep("restore beatmap", () =>
|
||||
{
|
||||
@ -883,7 +884,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("start match by other user", () =>
|
||||
{
|
||||
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
||||
multiplayerClient.StartMatch();
|
||||
multiplayerClient.StartMatch().WaitSafely();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||
|
@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddUntilStep("wait for spectating user state", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton.ReadyButton>();
|
||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||
|
||||
AddUntilStep("match started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
@ -129,6 +131,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQueueTabCount()
|
||||
{
|
||||
assertQueueTabCount(1);
|
||||
|
||||
addItemStep();
|
||||
assertQueueTabCount(2);
|
||||
|
||||
addItemStep();
|
||||
assertQueueTabCount(3);
|
||||
|
||||
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
|
||||
assertQueueTabCount(2);
|
||||
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
assertQueueTabCount(0);
|
||||
}
|
||||
|
||||
[Ignore("Expired items are initially removed from the room.")]
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
@ -213,6 +234,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
private void assertQueueTabCount(int count)
|
||||
{
|
||||
string queueTabText = count > 0 ? $"Queue ({count})" : "Queue";
|
||||
AddUntilStep($"Queue tab shows \"{queueTabText}\"", () =>
|
||||
{
|
||||
return this.ChildrenOfType<OsuTabControl<MultiplayerPlaylistDisplayMode>.OsuTabItem>()
|
||||
.Single(t => t.Value == MultiplayerPlaylistDisplayMode.Queue)
|
||||
.ChildrenOfType<OsuSpriteText>().Single().Text == queueTabText;
|
||||
});
|
||||
}
|
||||
|
||||
private void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
|
||||
|
||||
private bool inQueueList(int playlistItemId)
|
||||
|
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||
{
|
||||
private MultiplayerSpectateButton spectateButton;
|
||||
private MultiplayerReadyButton readyButton;
|
||||
private MatchStartControl startControl;
|
||||
|
||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||
|
||||
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(200, 50),
|
||||
},
|
||||
readyButton = new MultiplayerReadyButton
|
||||
startControl = new MatchStartControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@ -146,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||
|
||||
private void assertReadyButtonEnablement(bool shouldBeEnabled)
|
||||
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => readyButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -21,10 +23,12 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.Select.Options;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditComponentDuringGameplay()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
SkinEditor skinEditor = null;
|
||||
|
||||
AddStep("open skin editor", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
InputManager.PressKey(Key.ShiftLeft);
|
||||
InputManager.Key(Key.S);
|
||||
InputManager.ReleaseKey(Key.ControlLeft);
|
||||
InputManager.ReleaseKey(Key.ShiftLeft);
|
||||
});
|
||||
|
||||
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
|
||||
|
||||
AddStep("Click gameplay scene button", () =>
|
||||
{
|
||||
skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick();
|
||||
});
|
||||
|
||||
AddUntilStep("wait for player", () =>
|
||||
{
|
||||
// dismiss any notifications that may appear (ie. muted notification).
|
||||
clickMouseInCentre();
|
||||
return Game.ScreenStack.CurrentScreen is Player;
|
||||
});
|
||||
|
||||
BarHitErrorMeter hitErrorMeter = null;
|
||||
|
||||
AddUntilStep("select bar hit error blueprint", () =>
|
||||
{
|
||||
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
|
||||
|
||||
if (blueprint == null)
|
||||
return false;
|
||||
|
||||
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
|
||||
skinEditor.SelectedComponents.Clear();
|
||||
skinEditor.SelectedComponents.Add(blueprint.Item);
|
||||
return true;
|
||||
});
|
||||
|
||||
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
|
||||
|
||||
AddStep("hover first slider", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(
|
||||
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
|
||||
.ChildrenOfType<SettingsSlider<float>>().First()
|
||||
.ChildrenOfType<SliderBar<float>>().First()
|
||||
);
|
||||
});
|
||||
|
||||
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
|
||||
|
||||
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRetryCountIncrements()
|
||||
{
|
||||
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
||||
|
||||
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||
AddStep("show local scores",
|
||||
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||
|
||||
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
|
||||
|
||||
@ -152,7 +224,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
||||
|
||||
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||
AddStep("show local scores",
|
||||
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
|
||||
|
||||
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
|
||||
|
||||
@ -262,6 +335,20 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
exitViaBackButtonAndConfirm();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModsResetOnEnteringMultiplayer()
|
||||
{
|
||||
var osuAutomationMod = new OsuModAutoplay();
|
||||
|
||||
AddStep("Enable autoplay", () => { Game.SelectedMods.Value = new[] { osuAutomationMod }; });
|
||||
|
||||
PushAndConfirm(() => new Screens.OnlinePlay.Multiplayer.Multiplayer());
|
||||
AddUntilStep("Mods are removed", () => Game.SelectedMods.Value.Count == 0);
|
||||
|
||||
AddStep("Return to menu", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
AddUntilStep("Mods are restored", () => Game.SelectedMods.Value.Contains(osuAutomationMod));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitMultiWithEscape()
|
||||
{
|
||||
|
163
osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs
Normal file
163
osu.Game.Tests/Visual/Online/TestSceneChannelListItem.cs
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Chat.ChannelList;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneChannelListItem : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||
|
||||
[Cached]
|
||||
private readonly Bindable<Channel> selected = new Bindable<Channel>();
|
||||
|
||||
private static readonly List<Channel> channels = new List<Channel>
|
||||
{
|
||||
createPublicChannel("#public-channel"),
|
||||
createPublicChannel("#public-channel-long-name"),
|
||||
createPrivateChannel("test user", 2),
|
||||
createPrivateChannel("test user long name", 3),
|
||||
};
|
||||
|
||||
private readonly Dictionary<Channel, ChannelListItem> channelMap = new Dictionary<Channel, ChannelListItem>();
|
||||
|
||||
private FillFlowContainer flow;
|
||||
private OsuSpriteText selectedText;
|
||||
private OsuSpriteText leaveText;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
foreach (var item in channelMap.Values)
|
||||
item.Expire();
|
||||
|
||||
channelMap.Clear();
|
||||
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
selectedText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
leaveText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Height = 16,
|
||||
AlwaysPresent = true,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Width = 190,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background6,
|
||||
},
|
||||
flow = new FillFlowContainer
|
||||
{
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
selected.BindValueChanged(change =>
|
||||
{
|
||||
selectedText.Text = $"Selected Channel: {change.NewValue?.Name ?? "[null]"}";
|
||||
}, true);
|
||||
|
||||
foreach (var channel in channels)
|
||||
{
|
||||
var item = new ChannelListItem(channel);
|
||||
flow.Add(item);
|
||||
channelMap.Add(channel, item);
|
||||
item.OnRequestSelect += c => selected.Value = c;
|
||||
item.OnRequestLeave += leaveChannel;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVisual()
|
||||
{
|
||||
AddStep("Select second item", () => selected.Value = channels.Skip(1).First());
|
||||
|
||||
AddStep("Unread Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
channelMap[selected.Value].Unread.Value = true;
|
||||
});
|
||||
|
||||
AddStep("Read Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
channelMap[selected.Value].Unread.Value = false;
|
||||
});
|
||||
|
||||
AddStep("Add Mention Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
channelMap[selected.Value].Mentions.Value++;
|
||||
});
|
||||
|
||||
AddStep("Add 98 Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
channelMap[selected.Value].Mentions.Value += 98;
|
||||
});
|
||||
|
||||
AddStep("Clear Mentions Selected", () =>
|
||||
{
|
||||
if (selected.Value != null)
|
||||
channelMap[selected.Value].Mentions.Value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
private void leaveChannel(Channel channel)
|
||||
{
|
||||
leaveText.Text = $"OnRequestLeave: {channel.Name}";
|
||||
leaveText.FadeOutFromOne(1000, Easing.InQuint);
|
||||
}
|
||||
|
||||
private static Channel createPublicChannel(string name) =>
|
||||
new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
|
||||
|
||||
private static Channel createPrivateChannel(string username, int id)
|
||||
=> new Channel(new APIUser { Id = id, Username = username });
|
||||
}
|
||||
}
|
@ -40,6 +40,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
Text = @"You're a fake!",
|
||||
},
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = @"Careful with this one..",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using Realms;
|
||||
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
|
||||
[Ignored]
|
||||
public APIBeatmap? OnlineInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
|
||||
/// Todo: This should never be used nor exist, but is still relied on in <see cref="ScoresContainer.Scores"/> since <see cref="IBeatmapInfo"/> can't be used yet. For now this is obsoleted until it is removed.
|
||||
/// </summary>
|
||||
[Ignored]
|
||||
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
|
||||
public int? MaxCombo { get; set; }
|
||||
|
||||
[Ignored]
|
||||
|
@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
|
||||
[NotMapped]
|
||||
public APIBeatmap OnlineInfo { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public int? MaxCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The playable length in milliseconds of this beatmap.
|
||||
/// </summary>
|
||||
|
@ -295,7 +295,6 @@ namespace osu.Game.Database
|
||||
TimelineZoom = beatmap.TimelineZoom,
|
||||
Countdown = beatmap.Countdown,
|
||||
CountdownOffset = beatmap.CountdownOffset,
|
||||
MaxCombo = beatmap.MaxCombo,
|
||||
Bookmarks = beatmap.Bookmarks,
|
||||
BeatmapSet = realmBeatmapSet,
|
||||
};
|
||||
|
@ -28,6 +28,14 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
protected virtual bool AllowMultipleFires => false;
|
||||
|
||||
/// <summary>
|
||||
/// Specify a custom activation delay, overriding the game-wide user setting.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be used in special cases where we want to be extra sure the user knows what they are doing. An example is when changes would be lost.
|
||||
/// </remarks>
|
||||
protected virtual double? HoldActivationDelay => null;
|
||||
|
||||
public Bindable<double> Progress = new BindableDouble();
|
||||
|
||||
private Bindable<double> holdActivationDelay;
|
||||
@ -35,7 +43,9 @@ namespace osu.Game.Graphics.Containers
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
holdActivationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
|
||||
holdActivationDelay = HoldActivationDelay != null
|
||||
? new Bindable<double>(HoldActivationDelay.Value)
|
||||
: config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
|
||||
}
|
||||
|
||||
protected void BeginConfirm()
|
||||
|
@ -45,8 +45,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
}
|
||||
}
|
||||
|
||||
protected readonly Container ColourContainer;
|
||||
|
||||
private readonly Container backgroundContainer;
|
||||
private readonly Container colourContainer;
|
||||
private readonly Container glowContainer;
|
||||
private readonly Box leftGlow;
|
||||
private readonly Box centerGlow;
|
||||
@ -113,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
colourContainer = new Container
|
||||
ColourContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
@ -182,7 +183,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
buttonColour = value;
|
||||
updateGlow();
|
||||
colourContainer.Colour = value;
|
||||
ColourContainer.Colour = value;
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,11 +231,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
Alpha = 0.05f
|
||||
};
|
||||
|
||||
colourContainer.Add(flash);
|
||||
ColourContainer.Add(flash);
|
||||
flash.FadeOutFromOne(100).Expire();
|
||||
|
||||
clickAnimating = true;
|
||||
colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint)
|
||||
ColourContainer.ResizeWidthTo(ColourContainer.Width * 1.05f, 100, Easing.OutQuint)
|
||||
.OnComplete(_ =>
|
||||
{
|
||||
clickAnimating = false;
|
||||
@ -246,14 +247,14 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
colourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
|
||||
ColourContainer.ResizeWidthTo(hover_width * 0.98f, click_duration * 4, Easing.OutQuad);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
if (State == SelectionState.Selected)
|
||||
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
|
||||
ColourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
|
||||
@ -279,12 +280,12 @@ namespace osu.Game.Graphics.UserInterface
|
||||
if (newState == SelectionState.Selected)
|
||||
{
|
||||
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
|
||||
colourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
|
||||
ColourContainer.ResizeWidthTo(hover_width, hover_duration, Easing.OutElastic);
|
||||
glowContainer.FadeIn(hover_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
colourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
|
||||
ColourContainer.ResizeWidthTo(idle_width, hover_duration, Easing.OutElastic);
|
||||
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
|
||||
glowContainer.FadeOut(hover_duration, Easing.OutQuint);
|
||||
}
|
||||
|
@ -18,6 +18,6 @@ namespace osu.Game.Online.Multiplayer.Countdown
|
||||
/// How long the countdown should last.
|
||||
/// </summary>
|
||||
[Key(0)]
|
||||
public TimeSpan Delay { get; set; }
|
||||
public TimeSpan Duration { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using MessagePack;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
|
||||
namespace osu.Game.Online.Multiplayer
|
||||
{
|
||||
@ -16,9 +17,12 @@ namespace osu.Game.Online.Multiplayer
|
||||
public abstract class MultiplayerCountdown
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the countdown will end.
|
||||
/// The amount of time remaining in the countdown.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only sent once from the server upon initial retrieval of the <see cref="MultiplayerRoom"/> or via a <see cref="CountdownChangedEvent"/>.
|
||||
/// </remarks>
|
||||
[Key(0)]
|
||||
public DateTimeOffset EndTime { get; set; }
|
||||
public TimeSpan TimeRemaining { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -1046,6 +1046,10 @@ namespace osu.Game
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleSkinEditor:
|
||||
skinEditor.ToggleVisibility();
|
||||
return true;
|
||||
|
||||
case GlobalAction.ResetInputSettings:
|
||||
Host.ResetInputHandlers();
|
||||
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
||||
|
@ -173,7 +173,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
{
|
||||
Text = score.MaxCombo.ToLocalisableString(@"0\x"),
|
||||
Font = OsuFont.GetFont(size: text_size),
|
||||
#pragma warning disable 618
|
||||
Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
|
||||
#pragma warning restore 618
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
// TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`.
|
||||
var beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
#pragma warning disable 618
|
||||
MaxCombo = apiBeatmap.MaxCombo,
|
||||
#pragma warning restore 618
|
||||
Status = apiBeatmap.Status,
|
||||
MD5Hash = apiBeatmap.MD5Hash
|
||||
};
|
||||
|
171
osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
Normal file
171
osu.Game/Overlays/Chat/ChannelList/ChannelListItem.cs
Normal file
@ -0,0 +1,171 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Users.Drawables;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.ChannelList
|
||||
{
|
||||
public class ChannelListItem : OsuClickableContainer
|
||||
{
|
||||
public event Action<Channel>? OnRequestSelect;
|
||||
public event Action<Channel>? OnRequestLeave;
|
||||
|
||||
public readonly BindableInt Mentions = new BindableInt();
|
||||
|
||||
public readonly BindableBool Unread = new BindableBool();
|
||||
|
||||
private readonly Channel channel;
|
||||
|
||||
private Box? hoverBox;
|
||||
private Box? selectBox;
|
||||
private OsuSpriteText? text;
|
||||
private ChannelListItemCloseButton? close;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<Channel> selectedChannel { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
public ChannelListItem(Channel channel)
|
||||
{
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Height = 30;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hoverBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background3,
|
||||
Alpha = 0f,
|
||||
},
|
||||
selectBox = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
Alpha = 0f,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = 18, Right = 10 },
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
createIcon(),
|
||||
text = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Text = channel.Name,
|
||||
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
|
||||
Colour = colourProvider.Light3,
|
||||
Margin = new MarginPadding { Bottom = 2 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Truncate = true,
|
||||
},
|
||||
new ChannelListItemMentionPill
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 3 },
|
||||
Mentions = { BindTarget = Mentions },
|
||||
},
|
||||
close = new ChannelListItemCloseButton
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Right = 3 },
|
||||
Action = () => OnRequestLeave?.Invoke(channel),
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
Action = () => OnRequestSelect?.Invoke(channel);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedChannel.BindValueChanged(change =>
|
||||
{
|
||||
if (change.NewValue == channel)
|
||||
selectBox?.FadeIn(300, Easing.OutQuint);
|
||||
else
|
||||
selectBox?.FadeOut(200, Easing.OutQuint);
|
||||
}, true);
|
||||
|
||||
Unread.BindValueChanged(change =>
|
||||
{
|
||||
text!.FadeColour(change.NewValue ? colourProvider.Content1 : colourProvider.Light3, 300, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
hoverBox?.FadeIn(300, Easing.OutQuint);
|
||||
close?.FadeIn(300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
hoverBox?.FadeOut(200, Easing.OutQuint);
|
||||
close?.FadeOut(200, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private Drawable createIcon()
|
||||
{
|
||||
if (channel.Type != ChannelType.PM)
|
||||
return Drawable.Empty();
|
||||
|
||||
return new UpdateableAvatar(channel.Users.First(), isInteractive: false)
|
||||
{
|
||||
Size = new Vector2(20),
|
||||
Margin = new MarginPadding { Right = 5 },
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
CornerRadius = 10,
|
||||
Masking = true,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.ChannelList
|
||||
{
|
||||
public class ChannelListItemCloseButton : OsuClickableContainer
|
||||
{
|
||||
private SpriteIcon icon = null!;
|
||||
|
||||
private Color4 normalColour;
|
||||
private Color4 hoveredColour;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour)
|
||||
{
|
||||
normalColour = osuColour.Red2;
|
||||
hoveredColour = Color4.White;
|
||||
|
||||
Alpha = 0f;
|
||||
Size = new Vector2(20);
|
||||
Add(icon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.75f),
|
||||
Icon = FontAwesome.Solid.TimesCircle,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = normalColour,
|
||||
});
|
||||
}
|
||||
|
||||
// Transforms matching OsuAnimatedButton
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
icon.FadeColour(hoveredColour, 300, Easing.OutQuint);
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
icon.FadeColour(normalColour, 300, Easing.OutQuint);
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
icon.ScaleTo(0.75f, 2000, Easing.OutQuint);
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
icon.ScaleTo(1, 1000, Easing.OutElastic);
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Chat.ChannelList
|
||||
{
|
||||
public class ChannelListItemMentionPill : CircularContainer
|
||||
{
|
||||
public readonly BindableInt Mentions = new BindableInt();
|
||||
|
||||
private OsuSpriteText countText = null!;
|
||||
|
||||
private Box box = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour osuColour, OverlayColourProvider colourProvider)
|
||||
{
|
||||
Masking = true;
|
||||
Size = new Vector2(20, 12);
|
||||
Alpha = 0f;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
box = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = osuColour.Orange1,
|
||||
},
|
||||
countText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.Torus.With(size: 11, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Bottom = 1 },
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Mentions.BindValueChanged(change =>
|
||||
{
|
||||
int mentionCount = change.NewValue;
|
||||
|
||||
countText.Text = mentionCount > 99 ? "99+" : mentionCount.ToString();
|
||||
|
||||
if (mentionCount > 0)
|
||||
{
|
||||
this.FadeIn(1000, Easing.OutQuint);
|
||||
box.FlashColour(Color4.White, 500, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
this.FadeOut(100, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
@ -219,7 +219,12 @@ namespace osu.Game.Overlays.Dialog
|
||||
/// <summary>
|
||||
/// Programmatically clicks the first <see cref="PopupDialogOkButton"/>.
|
||||
/// </summary>
|
||||
public void PerformOkAction() => Buttons.OfType<PopupDialogOkButton>().First().TriggerClick();
|
||||
public void PerformOkAction() => PerformAction<PopupDialogOkButton>();
|
||||
|
||||
/// <summary>
|
||||
/// Programmatically clicks the first button of the provided type.
|
||||
/// </summary>
|
||||
public void PerformAction<T>() where T : PopupDialogButton => Buttons.OfType<T>().First().TriggerClick();
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
|
59
osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
Normal file
59
osu.Game/Overlays/Dialog/PopupDialogDangerousButton.cs
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.Dialog
|
||||
{
|
||||
public class PopupDialogDangerousButton : PopupDialogButton
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
ButtonColour = colours.Red3;
|
||||
|
||||
ColourContainer.Add(new ConfirmFillBox
|
||||
{
|
||||
Action = () => Action(),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
});
|
||||
}
|
||||
|
||||
private class ConfirmFillBox : HoldToConfirmContainer
|
||||
{
|
||||
private Box box;
|
||||
|
||||
protected override double? HoldActivationDelay => 500;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Child = box = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
Progress.BindValueChanged(progress => box.Width = (float)progress.NewValue, true);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
BeginConfirm();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
if (!e.HasAnyButtonPressed)
|
||||
AbortConfirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new SettingsButton
|
||||
{
|
||||
Text = SkinSettingsStrings.SkinLayoutEditor,
|
||||
Action = () => skinEditor?.Toggle(),
|
||||
Action = () => skinEditor?.ToggleVisibility(),
|
||||
},
|
||||
new ExportSkinButton(),
|
||||
};
|
||||
|
@ -500,12 +500,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
|
||||
|
||||
public bool Equals(LegacyHitSampleInfo? other)
|
||||
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank;
|
||||
// The additions to equality checks here are *required* to ensure that pooling works correctly.
|
||||
// Of note, `IsLayered` may cause the usage of `SampleVirtual` instead of an actual sample (in cases playback is not required).
|
||||
// Removing it would cause samples which may actually require playback to potentially source for a `SampleVirtual` sample pool.
|
||||
=> base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered;
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is LegacyHitSampleInfo other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank);
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
|
||||
}
|
||||
|
||||
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||
|
@ -122,7 +122,19 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public static class HitResultExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases/decreases the combo, and affects the combo portion of the score.
|
||||
/// Whether a <see cref="HitResult"/> increases the combo.
|
||||
/// </summary>
|
||||
public static bool IncreasesCombo(this HitResult result)
|
||||
=> AffectsCombo(result) && IsHit(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> breaks the combo and resets it back to zero.
|
||||
/// </summary>
|
||||
public static bool BreaksCombo(this HitResult result)
|
||||
=> AffectsCombo(result) && !IsHit(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> increases/breaks the combo, and affects the combo portion of the score.
|
||||
/// </summary>
|
||||
public static bool AffectsCombo(this HitResult result)
|
||||
{
|
||||
|
@ -166,20 +166,10 @@ namespace osu.Game.Rulesets.Scoring
|
||||
if (!result.Type.IsScorable())
|
||||
return;
|
||||
|
||||
if (result.Type.AffectsCombo())
|
||||
{
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
Combo.Value = 0;
|
||||
break;
|
||||
|
||||
default:
|
||||
Combo.Value++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (result.Type.IncreasesCombo())
|
||||
Combo.Value++;
|
||||
else if (result.Type.BreaksCombo())
|
||||
Combo.Value = 0;
|
||||
|
||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
||||
|
||||
|
@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </param>
|
||||
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
||||
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
|
||||
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
||||
public void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
|
||||
where TObject : HitObject
|
||||
where TDrawable : DrawableHitObject, new()
|
||||
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Scoring
|
||||
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// </summary>
|
||||
[Ignored]
|
||||
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
|
||||
|
@ -134,35 +134,9 @@ namespace osu.Game.Scoring
|
||||
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
||||
return score.TotalScore;
|
||||
|
||||
int beatmapMaxCombo;
|
||||
|
||||
if (score.IsLegacyScore)
|
||||
{
|
||||
// This score is guaranteed to be an osu!stable score.
|
||||
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
|
||||
if (score.BeatmapInfo.MaxCombo != null)
|
||||
beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value;
|
||||
else
|
||||
{
|
||||
if (difficulties == null)
|
||||
return score.TotalScore;
|
||||
|
||||
// We can compute the max combo locally after the async beatmap difficulty computation.
|
||||
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Something failed during difficulty calculation. Fall back to provided score.
|
||||
if (difficulty == null)
|
||||
return score.TotalScore;
|
||||
|
||||
beatmapMaxCombo = difficulty.Value.MaxCombo;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is guaranteed to be a non-legacy score.
|
||||
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
|
||||
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
|
||||
}
|
||||
int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false);
|
||||
if (beatmapMaxCombo == null)
|
||||
return score.TotalScore;
|
||||
|
||||
if (beatmapMaxCombo == 0)
|
||||
return 0;
|
||||
@ -171,7 +145,37 @@ namespace osu.Game.Scoring
|
||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
scoreProcessor.Mods.Value = score.Mods;
|
||||
|
||||
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo));
|
||||
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the maximum achievable combo for the provided score.
|
||||
/// </summary>
|
||||
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
|
||||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
|
||||
/// <returns>The maximum achievable combo. A <see langword="null"/> return value indicates the difficulty cache has failed to retrieve the combo.</returns>
|
||||
public async Task<int?> GetMaximumAchievableComboAsync([NotNull] ScoreInfo score, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (score.IsLegacyScore)
|
||||
{
|
||||
// This score is guaranteed to be an osu!stable score.
|
||||
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
|
||||
#pragma warning disable CS0618
|
||||
if (score.BeatmapInfo.MaxCombo != null)
|
||||
return score.BeatmapInfo.MaxCombo.Value;
|
||||
#pragma warning restore CS0618
|
||||
|
||||
if (difficulties == null)
|
||||
return null;
|
||||
|
||||
// We can compute the max combo locally after the async beatmap difficulty computation.
|
||||
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
|
||||
return difficulty?.MaxCombo;
|
||||
}
|
||||
|
||||
// This is guaranteed to be a non-legacy score.
|
||||
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
|
||||
return Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private bool canSave;
|
||||
|
||||
private bool exitConfirmed;
|
||||
protected bool ExitConfirmed { get; private set; }
|
||||
|
||||
private string lastSavedHash;
|
||||
|
||||
@ -586,7 +586,7 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
if (!exitConfirmed)
|
||||
if (!ExitConfirmed)
|
||||
{
|
||||
// dialog overlay may not be available in visual tests.
|
||||
if (dialogOverlay == null)
|
||||
@ -595,12 +595,9 @@ namespace osu.Game.Screens.Edit
|
||||
return true;
|
||||
}
|
||||
|
||||
// if the dialog is already displayed, confirm exit with no save.
|
||||
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
|
||||
{
|
||||
saveDialog.PerformOkAction();
|
||||
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
|
||||
if (dialogOverlay.CurrentDialog is PromptForSaveDialog)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNewBeatmap || HasUnsavedChanges)
|
||||
{
|
||||
@ -645,7 +642,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
Save();
|
||||
|
||||
exitConfirmed = true;
|
||||
ExitConfirmed = true;
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
@ -668,7 +665,7 @@ namespace osu.Game.Screens.Edit
|
||||
Beatmap.SetDefault();
|
||||
}
|
||||
|
||||
exitConfirmed = true;
|
||||
ExitConfirmed = true;
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
|
@ -17,12 +17,12 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
Buttons = new PopupDialogButton[]
|
||||
{
|
||||
new PopupDialogCancelButton
|
||||
new PopupDialogOkButton
|
||||
{
|
||||
Text = @"Save my masterpiece!",
|
||||
Action = saveAndExit
|
||||
},
|
||||
new PopupDialogOkButton
|
||||
new PopupDialogDangerousButton
|
||||
{
|
||||
Text = @"Forget all changes",
|
||||
Action = exit
|
||||
|
@ -14,18 +14,20 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
public abstract class ReadyButton : TriangleButton, IHasTooltip
|
||||
{
|
||||
public new readonly BindableBool Enabled = new BindableBool();
|
||||
protected readonly IBindable<BeatmapAvailability> Availability = new Bindable<BeatmapAvailability>();
|
||||
|
||||
private readonly IBindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
|
||||
{
|
||||
Availability.BindTo(beatmapTracker.Availability);
|
||||
Availability.BindValueChanged(_ => updateState());
|
||||
availability.BindTo(beatmapTracker.Availability);
|
||||
|
||||
availability.BindValueChanged(_ => updateState());
|
||||
Enabled.BindValueChanged(_ => updateState(), true);
|
||||
}
|
||||
|
||||
private void updateState() =>
|
||||
base.Enabled.Value = Availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
|
||||
base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value;
|
||||
|
||||
public virtual LocalisableString TooltipText
|
||||
{
|
||||
@ -34,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
if (Enabled.Value)
|
||||
return string.Empty;
|
||||
|
||||
if (Availability.Value.State != DownloadState.LocallyAvailable)
|
||||
if (availability.Value.State != DownloadState.LocallyAvailable)
|
||||
return "Beatmap not downloaded";
|
||||
|
||||
return string.Empty;
|
||||
|
@ -0,0 +1,224 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public class MatchStartControl : MultiplayerRoomComposite
|
||||
{
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private IDisposable clickOperation;
|
||||
|
||||
private Sample sampleReady;
|
||||
private Sample sampleReadyAll;
|
||||
private Sample sampleUnready;
|
||||
|
||||
private readonly BindableBool enabled = new BindableBool();
|
||||
private readonly MultiplayerCountdownButton countdownButton;
|
||||
private int countReady;
|
||||
private ScheduledDelegate readySampleDelegate;
|
||||
private IBindable<bool> operationInProgress;
|
||||
|
||||
public MatchStartControl()
|
||||
{
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new MultiplayerReadyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Action = onReadyClick,
|
||||
Enabled = { BindTarget = enabled },
|
||||
},
|
||||
countdownButton = new MultiplayerCountdownButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(40, 1),
|
||||
Alpha = 0,
|
||||
Action = startCountdown,
|
||||
Enabled = { BindTarget = enabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
|
||||
operationInProgress.BindValueChanged(_ => updateState());
|
||||
|
||||
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
|
||||
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
updateState();
|
||||
}
|
||||
|
||||
protected override void OnRoomLoadRequested()
|
||||
{
|
||||
base.OnRoomLoadRequested();
|
||||
endOperation();
|
||||
}
|
||||
|
||||
private void onReadyClick()
|
||||
{
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
|
||||
if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero)
|
||||
{
|
||||
toggleReady();
|
||||
return;
|
||||
}
|
||||
|
||||
// Local user is the room host and is in a ready state.
|
||||
// The only action they can take is to stop a countdown if one's currently running.
|
||||
if (Room.Countdown != null)
|
||||
{
|
||||
stopCountdown();
|
||||
return;
|
||||
}
|
||||
|
||||
// And if a countdown isn't running, start the match.
|
||||
startMatch();
|
||||
|
||||
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||
|
||||
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
|
||||
|
||||
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
|
||||
|
||||
void startMatch() => Client.StartMatch().ContinueWith(t =>
|
||||
{
|
||||
// accessing Exception here silences any potential errors from the antecedent task
|
||||
if (t.Exception != null)
|
||||
{
|
||||
// gameplay was not started due to an exception; unblock button.
|
||||
endOperation();
|
||||
}
|
||||
|
||||
// gameplay is starting, the button will be unblocked on load requested.
|
||||
});
|
||||
}
|
||||
|
||||
private void startCountdown(TimeSpan duration)
|
||||
{
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
Client.SendMatchRequest(new StartMatchCountdownRequest { Duration = duration }).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void endOperation()
|
||||
{
|
||||
clickOperation?.Dispose();
|
||||
clickOperation = null;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Room == null)
|
||||
{
|
||||
enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var localUser = Client.LocalUser;
|
||||
|
||||
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
|
||||
if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero)
|
||||
countdownButton.Alpha = 0;
|
||||
else
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
countdownButton.Alpha = 0;
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
enabled.Value =
|
||||
Room.State == MultiplayerRoomState.Open
|
||||
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
||||
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
||||
&& !operationInProgress.Value;
|
||||
|
||||
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
|
||||
|
||||
if (newCountReady == countReady)
|
||||
return;
|
||||
|
||||
readySampleDelegate?.Cancel();
|
||||
readySampleDelegate = Schedule(() =>
|
||||
{
|
||||
if (newCountReady > countReady)
|
||||
{
|
||||
if (newCountReady == newCountTotal)
|
||||
sampleReadyAll?.Play();
|
||||
else
|
||||
sampleReady?.Play();
|
||||
}
|
||||
else if (newCountReady < countReady)
|
||||
{
|
||||
sampleUnready?.Play();
|
||||
}
|
||||
|
||||
countReady = newCountReady;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public class MultiplayerCountdownButton : IconButton, IHasPopover
|
||||
{
|
||||
private static readonly TimeSpan[] available_delays =
|
||||
{
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30),
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(2)
|
||||
};
|
||||
|
||||
public new Action<TimeSpan> Action;
|
||||
|
||||
private readonly Drawable background;
|
||||
|
||||
public MultiplayerCountdownButton()
|
||||
{
|
||||
Icon = FontAwesome.Solid.CaretDown;
|
||||
IconScale = new Vector2(0.6f);
|
||||
|
||||
Add(background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
|
||||
base.Action = this.ShowPopover;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
background.Colour = colours.Green;
|
||||
}
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
var flow = new FillFlowContainer
|
||||
{
|
||||
Width = 200,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2),
|
||||
};
|
||||
|
||||
foreach (var duration in available_delays)
|
||||
{
|
||||
flow.Add(new OsuButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = $"Start match in {duration.Humanize()}",
|
||||
BackgroundColour = background.Colour,
|
||||
Action = () =>
|
||||
{
|
||||
Action(duration);
|
||||
this.HidePopover();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new OsuPopover { Child = flow };
|
||||
}
|
||||
}
|
||||
}
|
@ -28,7 +28,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
null,
|
||||
new MultiplayerReadyButton
|
||||
new MatchStartControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
|
@ -2,465 +2,176 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Backgrounds;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.Countdown;
|
||||
using osuTK;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||
{
|
||||
public class MultiplayerReadyButton : MultiplayerRoomComposite
|
||||
public class MultiplayerReadyButton : ReadyButton
|
||||
{
|
||||
public new Triangles Triangles => base.Triangles;
|
||||
|
||||
[Resolved]
|
||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private IDisposable clickOperation;
|
||||
|
||||
private Sample sampleReady;
|
||||
private Sample sampleReadyAll;
|
||||
private Sample sampleUnready;
|
||||
|
||||
private readonly BindableBool enabled = new BindableBool();
|
||||
private readonly CountdownButton countdownButton;
|
||||
private int countReady;
|
||||
private ScheduledDelegate readySampleDelegate;
|
||||
private IBindable<bool> operationInProgress;
|
||||
|
||||
public MultiplayerReadyButton()
|
||||
{
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new ReadyButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Size = Vector2.One,
|
||||
Action = onReadyClick,
|
||||
Enabled = { BindTarget = enabled },
|
||||
},
|
||||
countdownButton = new CountdownButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(40, 1),
|
||||
Alpha = 0,
|
||||
Action = startCountdown,
|
||||
Enabled = { BindTarget = enabled }
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy();
|
||||
operationInProgress.BindValueChanged(_ => updateState());
|
||||
|
||||
sampleReady = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||
sampleReadyAll = audio.Samples.Get(@"Multiplayer/player-ready-all");
|
||||
sampleUnready = audio.Samples.Get(@"Multiplayer/player-unready");
|
||||
}
|
||||
private MultiplayerRoom room => multiplayerClient.Room;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
||||
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||
onRoomUpdated();
|
||||
}
|
||||
|
||||
protected override void OnRoomUpdated()
|
||||
private MultiplayerCountdown countdown;
|
||||
private DateTimeOffset countdownReceivedTime;
|
||||
private ScheduledDelegate countdownUpdateDelegate;
|
||||
|
||||
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||
{
|
||||
base.OnRoomUpdated();
|
||||
updateState();
|
||||
}
|
||||
if (countdown == null && room?.Countdown != null)
|
||||
countdownReceivedTime = DateTimeOffset.Now;
|
||||
|
||||
protected override void OnRoomLoadRequested()
|
||||
{
|
||||
base.OnRoomLoadRequested();
|
||||
endOperation();
|
||||
}
|
||||
countdown = room?.Countdown;
|
||||
|
||||
private void onReadyClick()
|
||||
{
|
||||
if (Room == null)
|
||||
return;
|
||||
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
// Ensure the current user becomes ready before being able to do anything else (start match, stop countdown, unready).
|
||||
if (!isReady() || !Client.IsHost || Room.Settings.AutoStartDuration != TimeSpan.Zero)
|
||||
if (room?.Countdown != null)
|
||||
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
|
||||
else
|
||||
{
|
||||
toggleReady();
|
||||
countdownUpdateDelegate?.Cancel();
|
||||
countdownUpdateDelegate = null;
|
||||
}
|
||||
|
||||
updateButtonText();
|
||||
updateButtonColour();
|
||||
});
|
||||
|
||||
private void updateButtonText()
|
||||
{
|
||||
if (room == null)
|
||||
{
|
||||
Text = "Ready";
|
||||
return;
|
||||
}
|
||||
|
||||
// Local user is the room host and is in a ready state.
|
||||
// The only action they can take is to stop a countdown if one's currently running.
|
||||
if (Room.Countdown != null)
|
||||
var localUser = multiplayerClient.LocalUser;
|
||||
|
||||
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
string countText = $"({countReady} / {countTotal} ready)";
|
||||
|
||||
if (countdown != null)
|
||||
{
|
||||
stopCountdown();
|
||||
return;
|
||||
}
|
||||
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
|
||||
TimeSpan countdownRemaining;
|
||||
|
||||
// And if a countdown isn't running, start the match.
|
||||
startMatch();
|
||||
if (timeElapsed > countdown.TimeRemaining)
|
||||
countdownRemaining = TimeSpan.Zero;
|
||||
else
|
||||
countdownRemaining = countdown.TimeRemaining - timeElapsed;
|
||||
|
||||
bool isReady() => Client.LocalUser?.State == MultiplayerUserState.Ready || Client.LocalUser?.State == MultiplayerUserState.Spectating;
|
||||
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
|
||||
|
||||
void toggleReady() => Client.ToggleReady().ContinueWith(_ => endOperation());
|
||||
|
||||
void stopCountdown() => Client.SendMatchRequest(new StopCountdownRequest()).ContinueWith(_ => endOperation());
|
||||
|
||||
void startMatch() => Client.StartMatch().ContinueWith(t =>
|
||||
{
|
||||
// accessing Exception here silences any potential errors from the antecedent task
|
||||
if (t.Exception != null)
|
||||
switch (localUser?.State)
|
||||
{
|
||||
// gameplay was not started due to an exception; unblock button.
|
||||
endOperation();
|
||||
default:
|
||||
Text = $"Ready ({countdownText.ToLowerInvariant()})";
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
Text = $"{countdownText} {countText}";
|
||||
break;
|
||||
}
|
||||
|
||||
// gameplay is starting, the button will be unblocked on load requested.
|
||||
});
|
||||
}
|
||||
|
||||
private void startCountdown(TimeSpan duration)
|
||||
{
|
||||
Debug.Assert(clickOperation == null);
|
||||
clickOperation = ongoingOperationTracker.BeginOperation();
|
||||
|
||||
Client.SendMatchRequest(new StartMatchCountdownRequest { Delay = duration }).ContinueWith(_ => endOperation());
|
||||
}
|
||||
|
||||
private void endOperation()
|
||||
{
|
||||
clickOperation?.Dispose();
|
||||
clickOperation = null;
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Room == null)
|
||||
{
|
||||
enabled.Value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var localUser = Client.LocalUser;
|
||||
|
||||
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
|
||||
if (Room.Countdown != null || Room.Settings.AutoStartDuration != TimeSpan.Zero)
|
||||
countdownButton.Alpha = 0;
|
||||
else
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
countdownButton.Alpha = 0;
|
||||
Text = "Ready";
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
countdownButton.Alpha = Room.Host?.Equals(localUser) == true ? 1 : 0;
|
||||
Text = room.Host?.Equals(localUser) == true
|
||||
? $"Start match {countText}"
|
||||
: $"Waiting for host... {countText}";
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enabled.Value =
|
||||
Room.State == MultiplayerRoomState.Open
|
||||
&& CurrentPlaylistItem.Value?.ID == Room.Settings.PlaylistItemId
|
||||
&& !Room.Playlist.Single(i => i.ID == Room.Settings.PlaylistItemId).Expired
|
||||
&& !operationInProgress.Value;
|
||||
|
||||
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
|
||||
if (localUser?.State == MultiplayerUserState.Spectating)
|
||||
enabled.Value &= Room.Host?.Equals(localUser) == true && newCountReady > 0;
|
||||
|
||||
if (newCountReady == countReady)
|
||||
private void updateButtonColour()
|
||||
{
|
||||
if (room == null)
|
||||
{
|
||||
setGreen();
|
||||
return;
|
||||
|
||||
readySampleDelegate?.Cancel();
|
||||
readySampleDelegate = Schedule(() =>
|
||||
{
|
||||
if (newCountReady > countReady)
|
||||
{
|
||||
if (newCountReady == newCountTotal)
|
||||
sampleReadyAll?.Play();
|
||||
else
|
||||
sampleReady?.Play();
|
||||
}
|
||||
else if (newCountReady < countReady)
|
||||
{
|
||||
sampleUnready?.Play();
|
||||
}
|
||||
|
||||
countReady = newCountReady;
|
||||
});
|
||||
}
|
||||
|
||||
public class ReadyButton : Components.ReadyButton
|
||||
{
|
||||
public new Triangles Triangles => base.Triangles;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient multiplayerClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[CanBeNull]
|
||||
private MultiplayerRoom room => multiplayerClient.Room;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
multiplayerClient.RoomUpdated += () => Scheduler.AddOnce(onRoomUpdated);
|
||||
onRoomUpdated();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
var localUser = multiplayerClient.LocalUser;
|
||||
|
||||
switch (localUser?.State)
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (room?.Countdown != null)
|
||||
{
|
||||
// Update the countdown timer.
|
||||
onRoomUpdated();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRoomUpdated()
|
||||
{
|
||||
updateButtonText();
|
||||
updateButtonColour();
|
||||
}
|
||||
|
||||
private void updateButtonText()
|
||||
{
|
||||
if (room == null)
|
||||
{
|
||||
Text = "Ready";
|
||||
return;
|
||||
}
|
||||
|
||||
var localUser = multiplayerClient.LocalUser;
|
||||
|
||||
int countReady = room.Users.Count(u => u.State == MultiplayerUserState.Ready);
|
||||
int countTotal = room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
|
||||
|
||||
string countdownText = room.Countdown == null ? string.Empty : $"Starting in {room.Countdown.EndTime - DateTimeOffset.Now:mm\\:ss}";
|
||||
string countText = $"({countReady} / {countTotal} ready)";
|
||||
|
||||
if (room.Countdown != null)
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
Text = $"Ready ({countdownText.ToLowerInvariant()})";
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
Text = $"{countdownText} {countText}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
Text = "Ready";
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
Text = room.Host?.Equals(localUser) == true
|
||||
? $"Start match {countText}"
|
||||
: $"Waiting for host... {countText}";
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateButtonColour()
|
||||
{
|
||||
if (room == null)
|
||||
{
|
||||
default:
|
||||
setGreen();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
var localUser = multiplayerClient.LocalUser;
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
|
||||
setGreen();
|
||||
else
|
||||
setYellow();
|
||||
|
||||
if (room.Countdown != null)
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
setGreen();
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
setYellow();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (localUser?.State)
|
||||
{
|
||||
default:
|
||||
setGreen();
|
||||
break;
|
||||
|
||||
case MultiplayerUserState.Spectating:
|
||||
case MultiplayerUserState.Ready:
|
||||
if (room?.Host?.Equals(localUser) == true)
|
||||
setGreen();
|
||||
else
|
||||
setYellow();
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void setYellow()
|
||||
{
|
||||
BackgroundColour = colours.YellowDark;
|
||||
Triangles.ColourDark = colours.YellowDark;
|
||||
Triangles.ColourLight = colours.Yellow;
|
||||
}
|
||||
|
||||
void setGreen()
|
||||
{
|
||||
BackgroundColour = colours.Green;
|
||||
Triangles.ColourDark = colours.Green;
|
||||
Triangles.ColourLight = colours.GreenLight;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
void setYellow()
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (multiplayerClient != null)
|
||||
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||
BackgroundColour = colours.YellowDark;
|
||||
Triangles.ColourDark = colours.YellowDark;
|
||||
Triangles.ColourLight = colours.Yellow;
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText
|
||||
void setGreen()
|
||||
{
|
||||
get
|
||||
{
|
||||
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
||||
return "Cancel countdown";
|
||||
|
||||
return base.TooltipText;
|
||||
}
|
||||
BackgroundColour = colours.Green;
|
||||
Triangles.ColourDark = colours.Green;
|
||||
Triangles.ColourLight = colours.GreenLight;
|
||||
}
|
||||
}
|
||||
|
||||
public class CountdownButton : IconButton, IHasPopover
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
private static readonly TimeSpan[] available_delays =
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (multiplayerClient != null)
|
||||
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||
}
|
||||
|
||||
public override LocalisableString TooltipText
|
||||
{
|
||||
get
|
||||
{
|
||||
TimeSpan.FromSeconds(10),
|
||||
TimeSpan.FromSeconds(30),
|
||||
TimeSpan.FromMinutes(1),
|
||||
TimeSpan.FromMinutes(2)
|
||||
};
|
||||
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
||||
return "Cancel countdown";
|
||||
|
||||
public new Action<TimeSpan> Action;
|
||||
|
||||
private readonly Drawable background;
|
||||
|
||||
public CountdownButton()
|
||||
{
|
||||
Icon = FontAwesome.Solid.CaretDown;
|
||||
IconScale = new Vector2(0.6f);
|
||||
|
||||
Add(background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MaxValue
|
||||
});
|
||||
|
||||
base.Action = this.ShowPopover;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
background.Colour = colours.Green;
|
||||
}
|
||||
|
||||
public Popover GetPopover()
|
||||
{
|
||||
var flow = new FillFlowContainer
|
||||
{
|
||||
Width = 200,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(2),
|
||||
};
|
||||
|
||||
foreach (var duration in available_delays)
|
||||
{
|
||||
flow.Add(new PopoverButton
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Text = $"Start match in {duration.Humanize()}",
|
||||
BackgroundColour = background.Colour,
|
||||
Action = () =>
|
||||
{
|
||||
Action(duration);
|
||||
this.HidePopover();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new OsuPopover { Child = flow };
|
||||
}
|
||||
|
||||
public class PopoverButton : OsuButton
|
||||
{
|
||||
return base.TooltipText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
@ -25,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
/// </summary>
|
||||
public Action<PlaylistItem> RequestEdit;
|
||||
|
||||
private MultiplayerPlaylistTabControl playlistTabControl;
|
||||
private MultiplayerQueueList queueList;
|
||||
private MultiplayerHistoryList historyList;
|
||||
private bool firstPopulation = true;
|
||||
@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new OsuTabControl<MultiplayerPlaylistDisplayMode>
|
||||
playlistTabControl = new MultiplayerPlaylistTabControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = tab_control_height,
|
||||
@ -64,6 +64,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
playlistTabControl.QueueItems.BindTarget = queueList.Items;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
|
@ -0,0 +1,39 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
||||
{
|
||||
public class MultiplayerPlaylistTabControl : OsuTabControl<MultiplayerPlaylistDisplayMode>
|
||||
{
|
||||
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
|
||||
|
||||
protected override TabItem<MultiplayerPlaylistDisplayMode> CreateTabItem(MultiplayerPlaylistDisplayMode value)
|
||||
{
|
||||
if (value == MultiplayerPlaylistDisplayMode.Queue)
|
||||
return new QueueTabItem { QueueItems = { BindTarget = QueueItems } };
|
||||
|
||||
return base.CreateTabItem(value);
|
||||
}
|
||||
|
||||
private class QueueTabItem : OsuTabItem
|
||||
{
|
||||
public readonly IBindableList<PlaylistItem> QueueItems = new BindableList<PlaylistItem>();
|
||||
|
||||
public QueueTabItem()
|
||||
: base(MultiplayerPlaylistDisplayMode.Queue)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
QueueItems.BindCollectionChanged((_, __) => Text.Text = QueueItems.Count > 0 ? $"Queue ({QueueItems.Count})" : "Queue", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -115,6 +115,8 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
this.FadeIn();
|
||||
waves.Show();
|
||||
|
||||
Mods.SetDefault();
|
||||
|
||||
if (loungeSubScreen.IsCurrentScreen())
|
||||
loungeSubScreen.OnEntering(last);
|
||||
else
|
||||
|
@ -4,12 +4,16 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osuTK;
|
||||
@ -19,11 +23,27 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
public class BarHitErrorMeter : HitErrorMeter
|
||||
{
|
||||
private const int judgement_line_width = 14;
|
||||
private const int judgement_line_height = 4;
|
||||
|
||||
[SettingSource("Judgement line thickness", "How thick the individual lines should be.")]
|
||||
public BindableNumber<float> JudgementLineThickness { get; } = new BindableNumber<float>(4)
|
||||
{
|
||||
MinValue = 1,
|
||||
MaxValue = 8,
|
||||
Precision = 0.1f,
|
||||
};
|
||||
|
||||
[SettingSource("Show moving average arrow", "Whether an arrow should move beneath the bar showing the average error.")]
|
||||
public Bindable<bool> ShowMovingAverage { get; } = new BindableBool(true);
|
||||
|
||||
[SettingSource("Centre marker style", "How to signify the centre of the display")]
|
||||
public Bindable<CentreMarkerStyles> CentreMarkerStyle { get; } = new Bindable<CentreMarkerStyles>(CentreMarkerStyles.Circle);
|
||||
|
||||
[SettingSource("Label style", "How to show early/late extremities")]
|
||||
public Bindable<LabelStyles> LabelStyle { get; } = new Bindable<LabelStyles>(LabelStyles.Icons);
|
||||
|
||||
private SpriteIcon arrow;
|
||||
private SpriteIcon iconEarly;
|
||||
private SpriteIcon iconLate;
|
||||
private Drawable labelEarly;
|
||||
private Drawable labelLate;
|
||||
|
||||
private Container colourBarsEarly;
|
||||
private Container colourBarsLate;
|
||||
@ -32,6 +52,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
private double maxHitWindow;
|
||||
|
||||
private double floatingAverage;
|
||||
private Container colourBars;
|
||||
private Container arrowContainer;
|
||||
|
||||
private (HitResult result, double length)[] hitWindows;
|
||||
|
||||
private const int max_concurrent_judgements = 50;
|
||||
|
||||
private Drawable[] centreMarkerDrawables;
|
||||
|
||||
private const int centre_marker_size = 8;
|
||||
|
||||
public BarHitErrorMeter()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
@ -40,13 +72,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const int centre_marker_size = 8;
|
||||
const int bar_height = 200;
|
||||
const int bar_width = 2;
|
||||
const float chevron_size = 8;
|
||||
const float icon_size = 14;
|
||||
|
||||
var hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||
hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
@ -65,22 +95,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
iconEarly = new SpriteIcon
|
||||
{
|
||||
Y = -10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.ShippingFast,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
iconLate = new SpriteIcon
|
||||
{
|
||||
Y = 10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.Bicycle,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
colourBarsEarly = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@ -98,14 +112,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Height = 0.5f,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Name = "middle marker behind",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(centre_marker_size),
|
||||
},
|
||||
judgementsContainer = new Container
|
||||
{
|
||||
Name = "judgements",
|
||||
@ -114,24 +120,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = judgement_line_width,
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Name = "middle marker in front",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(centre_marker_size),
|
||||
Scale = new Vector2(0.5f),
|
||||
},
|
||||
}
|
||||
},
|
||||
new Container
|
||||
arrowContainer = new Container
|
||||
{
|
||||
Name = "average chevron",
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreRight,
|
||||
Width = chevron_size,
|
||||
X = chevron_size,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Alpha = 0,
|
||||
Scale = new Vector2(0, 1),
|
||||
Child = arrow = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@ -155,8 +155,180 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
colourBars.Height = 0;
|
||||
colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
|
||||
|
||||
arrow.Alpha = 0;
|
||||
arrow.Delay(200).FadeInFromZero(600);
|
||||
CentreMarkerStyle.BindValueChanged(style => recreateCentreMarker(style.NewValue), true);
|
||||
LabelStyle.BindValueChanged(style => recreateLabels(style.NewValue), true);
|
||||
|
||||
// delay the appearance animations for only the initial appearance.
|
||||
using (arrowContainer.BeginDelayedSequence(450))
|
||||
{
|
||||
ShowMovingAverage.BindValueChanged(visible =>
|
||||
{
|
||||
arrowContainer.FadeTo(visible.NewValue ? 1 : 0, 250, Easing.OutQuint);
|
||||
arrowContainer.ScaleTo(visible.NewValue ? new Vector2(1) : new Vector2(0, 1), 250, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
||||
private void recreateCentreMarker(CentreMarkerStyles style)
|
||||
{
|
||||
if (centreMarkerDrawables != null)
|
||||
{
|
||||
foreach (var d in centreMarkerDrawables)
|
||||
{
|
||||
d.ScaleTo(0, 500, Easing.OutQuint)
|
||||
.FadeOut(500, Easing.OutQuint);
|
||||
|
||||
d.Expire();
|
||||
}
|
||||
|
||||
centreMarkerDrawables = null;
|
||||
}
|
||||
|
||||
switch (style)
|
||||
{
|
||||
case CentreMarkerStyles.None:
|
||||
break;
|
||||
|
||||
case CentreMarkerStyles.Circle:
|
||||
centreMarkerDrawables = new Drawable[]
|
||||
{
|
||||
new Circle
|
||||
{
|
||||
Name = "middle marker behind",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MaxValue,
|
||||
Size = new Vector2(centre_marker_size),
|
||||
},
|
||||
new Circle
|
||||
{
|
||||
Name = "middle marker in front",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MinValue,
|
||||
Size = new Vector2(centre_marker_size / 2f),
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
case CentreMarkerStyles.Line:
|
||||
const float border_size = 1.5f;
|
||||
|
||||
centreMarkerDrawables = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Name = "middle marker behind",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MaxValue,
|
||||
Size = new Vector2(judgement_line_width, centre_marker_size / 3f),
|
||||
},
|
||||
new Box
|
||||
{
|
||||
Name = "middle marker in front",
|
||||
Colour = GetColourForHitResult(hitWindows.Last().result).Darken(0.3f),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Depth = float.MinValue,
|
||||
Size = new Vector2(judgement_line_width - border_size, centre_marker_size / 3f - border_size),
|
||||
},
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, null);
|
||||
}
|
||||
|
||||
if (centreMarkerDrawables != null)
|
||||
{
|
||||
foreach (var d in centreMarkerDrawables)
|
||||
{
|
||||
colourBars.Add(d);
|
||||
|
||||
d.FadeInFromZero(500, Easing.OutQuint)
|
||||
.ScaleTo(0).ScaleTo(1, 1000, Easing.OutElasticHalf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void recreateLabels(LabelStyles style)
|
||||
{
|
||||
const float icon_size = 14;
|
||||
|
||||
labelEarly?.Expire();
|
||||
labelEarly = null;
|
||||
|
||||
labelLate?.Expire();
|
||||
labelLate = null;
|
||||
|
||||
switch (style)
|
||||
{
|
||||
case LabelStyles.None:
|
||||
break;
|
||||
|
||||
case LabelStyles.Icons:
|
||||
labelEarly = new SpriteIcon
|
||||
{
|
||||
Y = -10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.ShippingFast,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
labelLate = new SpriteIcon
|
||||
{
|
||||
Y = 10,
|
||||
Size = new Vector2(icon_size),
|
||||
Icon = FontAwesome.Solid.Bicycle,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
case LabelStyles.Text:
|
||||
labelEarly = new OsuSpriteText
|
||||
{
|
||||
Y = -10,
|
||||
Text = "Early",
|
||||
Font = OsuFont.Default.With(size: 10),
|
||||
Height = 12,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
labelLate = new OsuSpriteText
|
||||
{
|
||||
Y = 10,
|
||||
Text = "Late",
|
||||
Font = OsuFont.Default.With(size: 10),
|
||||
Height = 12,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(style), style, null);
|
||||
}
|
||||
|
||||
if (labelEarly != null)
|
||||
{
|
||||
colourBars.Add(labelEarly);
|
||||
labelEarly.FadeInFromZero(500);
|
||||
}
|
||||
|
||||
if (labelLate != null)
|
||||
{
|
||||
colourBars.Add(labelLate);
|
||||
labelLate.FadeInFromZero(500);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -164,8 +336,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
base.Update();
|
||||
|
||||
// undo any layout rotation to display icons in the correct orientation
|
||||
iconEarly.Rotation = -Rotation;
|
||||
iconLate.Rotation = -Rotation;
|
||||
if (labelEarly != null) labelEarly.Rotation = -Rotation;
|
||||
if (labelLate != null) labelLate.Rotation = -Rotation;
|
||||
}
|
||||
|
||||
private void createColourBars((HitResult result, double length)[] windows)
|
||||
@ -224,11 +396,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
}
|
||||
}
|
||||
|
||||
private double floatingAverage;
|
||||
private Container colourBars;
|
||||
|
||||
private const int max_concurrent_judgements = 50;
|
||||
|
||||
protected override void OnNewJudgement(JudgementResult judgement)
|
||||
{
|
||||
const int arrow_move_duration = 800;
|
||||
@ -255,6 +422,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
judgementsContainer.Add(new JudgementLine
|
||||
{
|
||||
JudgementLineThickness = { BindTarget = JudgementLineThickness },
|
||||
Y = getRelativeJudgementPosition(judgement.TimeOffset),
|
||||
Colour = GetColourForHitResult(judgement.Type),
|
||||
});
|
||||
@ -268,11 +436,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
|
||||
internal class JudgementLine : CompositeDrawable
|
||||
{
|
||||
public readonly BindableNumber<float> JudgementLineThickness = new BindableFloat();
|
||||
|
||||
public JudgementLine()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Height = judgement_line_height;
|
||||
|
||||
Blending = BlendingParameters.Additive;
|
||||
|
||||
@ -295,6 +464,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
Alpha = 0;
|
||||
Width = 0;
|
||||
|
||||
JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true);
|
||||
|
||||
this
|
||||
.FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint)
|
||||
.ResizeWidthTo(1, judgement_fade_in_duration, Easing.OutQuint)
|
||||
@ -306,5 +477,19 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
||||
}
|
||||
|
||||
public override void Clear() => judgementsContainer.Clear();
|
||||
|
||||
public enum CentreMarkerStyles
|
||||
{
|
||||
None,
|
||||
Circle,
|
||||
Line
|
||||
}
|
||||
|
||||
public enum LabelStyles
|
||||
{
|
||||
None,
|
||||
Icons,
|
||||
Text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
{
|
||||
positionalAdjust = Vector2.Distance(e.ScreenSpaceMousePosition, button.ScreenSpaceDrawQuad.Centre) / 200;
|
||||
positionalAdjust = Vector2.Distance(e.MousePosition, button.ToSpaceOfOtherDrawable(button.DrawRectangle.Centre, Parent)) / 100;
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
|
@ -615,16 +615,22 @@ namespace osu.Game.Screens.Play
|
||||
/// <param name="time">The destination time to seek to.</param>
|
||||
internal void NonFrameStableSeek(double time)
|
||||
{
|
||||
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
|
||||
frameStablePlaybackResetDelegate.RunTask();
|
||||
// TODO: This schedule should not be required and is a temporary hotfix.
|
||||
// See https://github.com/ppy/osu/issues/17267 for the issue.
|
||||
// See https://github.com/ppy/osu/pull/17302 for a better fix which needs some more time.
|
||||
ScheduleAfterChildren(() =>
|
||||
{
|
||||
if (frameStablePlaybackResetDelegate?.Cancelled == false && !frameStablePlaybackResetDelegate.Completed)
|
||||
frameStablePlaybackResetDelegate.RunTask();
|
||||
|
||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||
DrawableRuleset.FrameStablePlayback = false;
|
||||
bool wasFrameStable = DrawableRuleset.FrameStablePlayback;
|
||||
DrawableRuleset.FrameStablePlayback = false;
|
||||
|
||||
Seek(time);
|
||||
Seek(time);
|
||||
|
||||
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||
// Delay resetting frame-stable playback for one frame to give the FrameStabilityContainer a chance to seek.
|
||||
frameStablePlaybackResetDelegate = ScheduleAfterChildren(() => DrawableRuleset.FrameStablePlayback = wasFrameStable);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1,10 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using ManagedBass.Fx;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -48,31 +49,31 @@ namespace osu.Game.Screens.Play
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
// We show the previous screen status
|
||||
protected override UserActivity InitialActivity => null;
|
||||
protected override UserActivity? InitialActivity => null;
|
||||
|
||||
protected override bool PlayResumeSound => false;
|
||||
|
||||
protected BeatmapMetadataDisplay MetadataInfo { get; private set; }
|
||||
protected BeatmapMetadataDisplay MetadataInfo { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A fill flow containing the player settings groups, exposed for the ability to hide it from inheritors of the player loader.
|
||||
/// </summary>
|
||||
protected FillFlowContainer<PlayerSettingsGroup> PlayerSettings { get; private set; }
|
||||
protected FillFlowContainer<PlayerSettingsGroup> PlayerSettings { get; private set; } = null!;
|
||||
|
||||
protected VisualSettings VisualSettings { get; private set; }
|
||||
protected VisualSettings VisualSettings { get; private set; } = null!;
|
||||
|
||||
protected AudioSettings AudioSettings { get; private set; }
|
||||
protected AudioSettings AudioSettings { get; private set; } = null!;
|
||||
|
||||
protected Task LoadTask { get; private set; }
|
||||
protected Task? LoadTask { get; private set; }
|
||||
|
||||
protected Task DisposalTask { get; private set; }
|
||||
protected Task? DisposalTask { get; private set; }
|
||||
|
||||
private bool backgroundBrightnessReduction;
|
||||
|
||||
private readonly BindableDouble volumeAdjustment = new BindableDouble(1);
|
||||
|
||||
private AudioFilter lowPassFilter;
|
||||
private AudioFilter highPassFilter;
|
||||
private AudioFilter lowPassFilter = null!;
|
||||
private AudioFilter highPassFilter = null!;
|
||||
|
||||
protected bool BackgroundBrightnessReduction
|
||||
{
|
||||
@ -90,47 +91,49 @@ namespace osu.Game.Screens.Play
|
||||
private bool readyForPush =>
|
||||
!playerConsumed
|
||||
// don't push unless the player is completely loaded
|
||||
&& player?.LoadState == LoadState.Ready
|
||||
&& CurrentPlayer?.LoadState == LoadState.Ready
|
||||
// don't push if the user is hovering one of the panes, unless they are idle.
|
||||
&& (IsHovered || idleTracker.IsIdle.Value)
|
||||
// don't push if the user is dragging a slider or otherwise.
|
||||
&& inputManager?.DraggedDrawable == null
|
||||
&& inputManager.DraggedDrawable == null
|
||||
// don't push if a focused overlay is visible, like settings.
|
||||
&& inputManager?.FocusedDrawable == null;
|
||||
&& inputManager.FocusedDrawable == null;
|
||||
|
||||
private readonly Func<Player> createPlayer;
|
||||
|
||||
private Player player;
|
||||
/// <summary>
|
||||
/// The <see cref="Player"/> instance being loaded by this screen.
|
||||
/// </summary>
|
||||
public Player? CurrentPlayer { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the curent player instance has been consumed via <see cref="consumePlayer"/>.
|
||||
/// Whether the current player instance has been consumed via <see cref="consumePlayer"/>.
|
||||
/// </summary>
|
||||
private bool playerConsumed;
|
||||
|
||||
private LogoTrackingContainer content;
|
||||
private LogoTrackingContainer content = null!;
|
||||
|
||||
private bool hideOverlays;
|
||||
|
||||
private InputManager inputManager;
|
||||
private InputManager inputManager = null!;
|
||||
|
||||
private IdleTracker idleTracker;
|
||||
private IdleTracker idleTracker = null!;
|
||||
|
||||
private ScheduledDelegate scheduledPushPlayer;
|
||||
private ScheduledDelegate? scheduledPushPlayer;
|
||||
|
||||
[CanBeNull]
|
||||
private EpilepsyWarning epilepsyWarning;
|
||||
private EpilepsyWarning? epilepsyWarning;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private NotificationOverlay notificationOverlay { get; set; }
|
||||
private NotificationOverlay? notificationOverlay { get; set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private VolumeOverlay volumeOverlay { get; set; }
|
||||
private VolumeOverlay? volumeOverlay { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private AudioManager audioManager { get; set; }
|
||||
private AudioManager audioManager { get; set; } = null!;
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private BatteryInfo batteryInfo { get; set; }
|
||||
private BatteryInfo? batteryInfo { get; set; }
|
||||
|
||||
public PlayerLoader(Func<Player> createPlayer)
|
||||
{
|
||||
@ -237,12 +240,14 @@ namespace osu.Game.Screens.Play
|
||||
{
|
||||
base.OnResuming(last);
|
||||
|
||||
var lastScore = player.Score;
|
||||
Debug.Assert(CurrentPlayer != null);
|
||||
|
||||
var lastScore = CurrentPlayer.Score;
|
||||
|
||||
AudioSettings.ReferenceScore.Value = lastScore?.ScoreInfo;
|
||||
|
||||
// prepare for a retry.
|
||||
player = null;
|
||||
CurrentPlayer = null;
|
||||
playerConsumed = false;
|
||||
cancelLoad();
|
||||
|
||||
@ -344,9 +349,10 @@ namespace osu.Game.Screens.Play
|
||||
private Player consumePlayer()
|
||||
{
|
||||
Debug.Assert(!playerConsumed);
|
||||
Debug.Assert(CurrentPlayer != null);
|
||||
|
||||
playerConsumed = true;
|
||||
return player;
|
||||
return CurrentPlayer;
|
||||
}
|
||||
|
||||
private void prepareNewPlayer()
|
||||
@ -354,11 +360,11 @@ namespace osu.Game.Screens.Play
|
||||
if (!this.IsCurrentScreen())
|
||||
return;
|
||||
|
||||
player = createPlayer();
|
||||
player.RestartCount = restartCount++;
|
||||
player.RestartRequested = restartRequested;
|
||||
CurrentPlayer = createPlayer();
|
||||
CurrentPlayer.RestartCount = restartCount++;
|
||||
CurrentPlayer.RestartRequested = restartRequested;
|
||||
|
||||
LoadTask = LoadComponentAsync(player, _ => MetadataInfo.Loading = false);
|
||||
LoadTask = LoadComponentAsync(CurrentPlayer, _ => MetadataInfo.Loading = false);
|
||||
}
|
||||
|
||||
private void restartRequested()
|
||||
@ -472,7 +478,7 @@ namespace osu.Game.Screens.Play
|
||||
if (isDisposing)
|
||||
{
|
||||
// if the player never got pushed, we should explicitly dispose it.
|
||||
DisposalTask = LoadTask?.ContinueWith(_ => player?.Dispose());
|
||||
DisposalTask = LoadTask?.ContinueWith(_ => CurrentPlayer?.Dispose());
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,7 +486,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
#region Mute warning
|
||||
|
||||
private Bindable<bool> muteWarningShownOnce;
|
||||
private Bindable<bool> muteWarningShownOnce = null!;
|
||||
|
||||
private int restartCount;
|
||||
|
||||
@ -535,7 +541,7 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
#region Low battery warning
|
||||
|
||||
private Bindable<bool> batteryWarningShownOnce;
|
||||
private Bindable<bool> batteryWarningShownOnce = null!;
|
||||
|
||||
private void showBatteryWarningIfNeeded()
|
||||
{
|
||||
|
@ -65,10 +65,12 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
|
||||
string creator = metadata.Author.Username;
|
||||
|
||||
int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely();
|
||||
|
||||
var topStatistics = new List<StatisticDisplay>
|
||||
{
|
||||
new AccuracyStatistic(score.Accuracy),
|
||||
new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out int missCount) || missCount == 0),
|
||||
new ComboStatistic(score.MaxCombo, beatmapMaxCombo),
|
||||
new PerformanceStatistic(score),
|
||||
};
|
||||
|
||||
@ -80,8 +82,6 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
statisticDisplays.AddRange(topStatistics);
|
||||
statisticDisplays.AddRange(bottomStatistics);
|
||||
|
||||
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
|
||||
|
||||
AddInternal(new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -224,6 +224,8 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
if (score.Date != default)
|
||||
AddInternal(new PlayedOnText(score.Date));
|
||||
|
||||
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
|
||||
|
||||
if (starDifficulty != null)
|
||||
{
|
||||
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)
|
||||
|
@ -25,11 +25,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
/// Creates a new <see cref="ComboStatistic"/>.
|
||||
/// </summary>
|
||||
/// <param name="combo">The combo to be displayed.</param>
|
||||
/// <param name="isPerfect">Whether this is a perfect combo.</param>
|
||||
public ComboStatistic(int combo, bool isPerfect)
|
||||
: base("combo", combo)
|
||||
/// <param name="maxCombo">The maximum value of <paramref name="combo"/>.</param>
|
||||
public ComboStatistic(int combo, int? maxCombo)
|
||||
: base("combo", combo, maxCombo)
|
||||
{
|
||||
this.isPerfect = isPerfect;
|
||||
isPerfect = combo == maxCombo;
|
||||
}
|
||||
|
||||
public override void Appear()
|
||||
|
@ -51,7 +51,7 @@ namespace osu.Game.Skinning.Editor
|
||||
fill.Clear();
|
||||
|
||||
var skinnableTypes = typeof(OsuGame).Assembly.GetTypes()
|
||||
.Where(t => !t.IsInterface)
|
||||
.Where(t => !t.IsInterface && !t.IsAbstract)
|
||||
.Where(t => typeof(ISkinnableDrawable).IsAssignableFrom(t))
|
||||
.OrderBy(t => t.Name)
|
||||
.ToArray();
|
||||
|
@ -19,15 +19,15 @@ namespace osu.Game.Skinning.Editor
|
||||
/// A container which handles loading a skin editor on user request for a specified target.
|
||||
/// This also handles the scaling / positioning adjustment of the target.
|
||||
/// </summary>
|
||||
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
public class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly ScalingContainer scalingContainer;
|
||||
|
||||
protected override bool BlockNonPositionalInput => true;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinEditor skinEditor;
|
||||
|
||||
public const float VISIBLE_TARGET_SCALE = 0.8f;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
@ -49,33 +49,13 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
Hide();
|
||||
return true;
|
||||
|
||||
case GlobalAction.ToggleSkinEditor:
|
||||
Toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
protected override void PopIn()
|
||||
{
|
||||
if (skinEditor == null)
|
||||
Show();
|
||||
else
|
||||
skinEditor.ToggleVisibility();
|
||||
}
|
||||
|
||||
public override void Hide()
|
||||
{
|
||||
// base call intentionally omitted.
|
||||
skinEditor?.Hide();
|
||||
}
|
||||
|
||||
public override void Show()
|
||||
{
|
||||
// base call intentionally omitted as we have custom behaviour.
|
||||
|
||||
if (skinEditor != null)
|
||||
{
|
||||
skinEditor.Show();
|
||||
@ -83,29 +63,24 @@ namespace osu.Game.Skinning.Editor
|
||||
}
|
||||
|
||||
var editor = new SkinEditor();
|
||||
|
||||
editor.State.BindValueChanged(visibility => updateComponentVisibility());
|
||||
|
||||
skinEditor = editor;
|
||||
|
||||
// Schedule ensures that if `Show` is called before this overlay is loaded,
|
||||
// it will not throw (LoadComponentAsync requires the load target to be in a loaded state).
|
||||
Schedule(() =>
|
||||
LoadComponentAsync(editor, _ =>
|
||||
{
|
||||
if (editor != skinEditor)
|
||||
return;
|
||||
|
||||
LoadComponentAsync(editor, _ =>
|
||||
{
|
||||
if (editor != skinEditor)
|
||||
return;
|
||||
AddInternal(editor);
|
||||
|
||||
AddInternal(editor);
|
||||
|
||||
SetTarget(lastTargetScreen);
|
||||
});
|
||||
SetTarget(lastTargetScreen);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PopOut() => skinEditor?.Hide();
|
||||
|
||||
private void updateComponentVisibility()
|
||||
{
|
||||
Debug.Assert(skinEditor != null);
|
||||
|
@ -94,7 +94,7 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
var replayGeneratingMod = ruleset.Value.CreateInstance().GetAutoplayMod();
|
||||
if (replayGeneratingMod != null)
|
||||
screen.Push(new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods)));
|
||||
screen.Push(new PlayerLoader(() => new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods))));
|
||||
}, new[] { typeof(Player), typeof(SongSelect) })
|
||||
},
|
||||
}
|
||||
@ -104,7 +104,7 @@ namespace osu.Game.Skinning.Editor
|
||||
};
|
||||
}
|
||||
|
||||
private class SceneButton : OsuButton
|
||||
public class SceneButton : OsuButton
|
||||
{
|
||||
public SceneButton()
|
||||
{
|
||||
|
@ -163,6 +163,12 @@ namespace osu.Game.Stores
|
||||
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
|
||||
}
|
||||
|
||||
protected override void UndeleteForReuse(BeatmapSetInfo existing)
|
||||
{
|
||||
base.UndeleteForReuse(existing);
|
||||
existing.DateAdded = DateTimeOffset.UtcNow;
|
||||
}
|
||||
|
||||
public override bool IsAvailableLocally(BeatmapSetInfo model)
|
||||
{
|
||||
return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
|
@ -351,7 +351,8 @@ namespace osu.Game.Stores
|
||||
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
existing.DeletePending = false;
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@ -387,7 +388,9 @@ namespace osu.Game.Stores
|
||||
{
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
existing.DeletePending = false;
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
@ -527,6 +530,15 @@ namespace osu.Game.Stores
|
||||
private bool checkAllFilesExist(TModel model) =>
|
||||
model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath()));
|
||||
|
||||
/// <summary>
|
||||
/// Called when an existing model is in a soft deleted state but being recovered.
|
||||
/// </summary>
|
||||
/// <param name="existing">The existing model.</param>
|
||||
protected virtual void UndeleteForReuse(TModel existing)
|
||||
{
|
||||
existing.DeletePending = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether this specified path should be removed after successful import.
|
||||
/// </summary>
|
||||
|
@ -7,10 +7,13 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -93,6 +96,10 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
protected class TestEditor : Editor
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
[CanBeNull]
|
||||
private DialogOverlay dialogOverlay { get; set; }
|
||||
|
||||
public new void Undo() => base.Undo();
|
||||
|
||||
public new void Redo() => base.Redo();
|
||||
@ -111,6 +118,18 @@ namespace osu.Game.Tests.Visual
|
||||
|
||||
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
||||
|
||||
public override bool OnExiting(IScreen next)
|
||||
{
|
||||
// For testing purposes allow the screen to exit without saving on second attempt.
|
||||
if (!ExitConfirmed && dialogOverlay?.CurrentDialog is PromptForSaveDialog saveDialog)
|
||||
{
|
||||
saveDialog.PerformAction<PopupDialogDangerousButton>();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnExiting(next);
|
||||
}
|
||||
|
||||
public TestEditor(EditorLoader loader = null)
|
||||
: base(loader)
|
||||
{
|
||||
|
@ -11,6 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
@ -296,11 +297,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private CancellationTokenSource? countdownFinishSource;
|
||||
private CancellationTokenSource? countdownSkipSource;
|
||||
private CancellationTokenSource? countdownStopSource;
|
||||
private Task countdownTask = Task.CompletedTask;
|
||||
|
||||
public void FinishCountDown() => countdownFinishSource?.Cancel();
|
||||
/// <summary>
|
||||
/// Skips to the end of the currently-running countdown, if one is running,
|
||||
/// and runs the callback (e.g. to start the match) as soon as possible unless the countdown has been cancelled.
|
||||
/// </summary>
|
||||
public void SkipToEndOfCountdown() => countdownSkipSource?.Cancel();
|
||||
|
||||
public override async Task SendMatchRequest(MatchUserRequest request)
|
||||
{
|
||||
@ -310,25 +315,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
switch (request)
|
||||
{
|
||||
case StartMatchCountdownRequest matchCountdownRequest:
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
countdownStopSource?.Cancel();
|
||||
|
||||
// Note that this will leak CTSs, however this is a test method and we haven't noticed foregoing disposal of non-linked CTSs to be detrimental.
|
||||
// If necessary, this can be moved into the final schedule below, and the class-level fields be nulled out accordingly.
|
||||
var stopSource = countdownStopSource = new CancellationTokenSource();
|
||||
var finishSource = countdownFinishSource = new CancellationTokenSource();
|
||||
var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, finishSource.Token);
|
||||
var countdown = new MatchStartCountdown { EndTime = DateTimeOffset.Now + matchCountdownRequest.Delay };
|
||||
var skipSource = countdownSkipSource = new CancellationTokenSource();
|
||||
var countdown = new MatchStartCountdown { TimeRemaining = matchCountdownRequest.Duration };
|
||||
|
||||
Task lastCountdownTask = countdownTask;
|
||||
countdownTask = start();
|
||||
|
||||
async Task start()
|
||||
{
|
||||
try
|
||||
{
|
||||
await lastCountdownTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
await lastCountdownTask;
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
@ -341,10 +343,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(matchCountdownRequest.Delay, cancellationSource.Token).ConfigureAwait(false);
|
||||
using (var cancellationSource = CancellationTokenSource.CreateLinkedTokenSource(stopSource.Token, skipSource.Token))
|
||||
await Task.Delay(matchCountdownRequest.Duration, cancellationSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Clients need to be notified of cancellations in the following code.
|
||||
}
|
||||
|
||||
Schedule(() =>
|
||||
@ -355,11 +359,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Room.Countdown = null;
|
||||
MatchEvent(new CountdownChangedEvent { Countdown = null });
|
||||
|
||||
using (cancellationSource)
|
||||
{
|
||||
if (stopSource.Token.IsCancellationRequested)
|
||||
return;
|
||||
}
|
||||
if (stopSource.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
StartMatch().WaitSafely();
|
||||
});
|
||||
@ -392,7 +393,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task StartMatch()
|
||||
public override Task StartMatch()
|
||||
{
|
||||
Debug.Assert(Room != null);
|
||||
|
||||
@ -400,7 +401,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready))
|
||||
ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad);
|
||||
|
||||
await ((IMultiplayerClient)this).LoadRequested();
|
||||
return ((IMultiplayerClient)this).LoadRequested();
|
||||
}
|
||||
|
||||
public override Task AbortGameplay()
|
||||
|
@ -88,7 +88,8 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
/// </summary>
|
||||
/// <param name="userId">The user to send frames for.</param>
|
||||
/// <param name="count">The total number of frames to send.</param>
|
||||
public void SendFramesFromUser(int userId, int count)
|
||||
/// <param name="startTime">The time to start gameplay frames from.</param>
|
||||
public void SendFramesFromUser(int userId, int count, double startTime = 0)
|
||||
{
|
||||
var frames = new List<LegacyReplayFrame>();
|
||||
|
||||
@ -102,7 +103,7 @@ namespace osu.Game.Tests.Visual.Spectator
|
||||
flush();
|
||||
|
||||
var buttonState = currentFrameIndex == lastFrameIndex ? ReplayButtonState.None : ReplayButtonState.Left1;
|
||||
frames.Add(new LegacyReplayFrame(currentFrameIndex * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||
frames.Add(new LegacyReplayFrame(currentFrameIndex * 100 + startTime, RNG.Next(0, 512), RNG.Next(0, 512), buttonState));
|
||||
}
|
||||
|
||||
flush();
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -82,7 +83,8 @@ namespace osu.Game.Updater
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.macOS:
|
||||
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal));
|
||||
string arch = RuntimeInformation.OSArchitecture == Architecture.Arm64 ? "Apple.Silicon" : "Intel";
|
||||
bestAsset = release.Assets?.Find(f => f.Name.EndsWith($".app.{arch}.zip", StringComparison.Ordinal));
|
||||
break;
|
||||
|
||||
case RuntimeInfo.Platform.Linux:
|
||||
|
@ -31,7 +31,7 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2021.1210.0">
|
||||
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.320.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
Loading…
Reference in New Issue
Block a user