mirror of
https://github.com/ppy/osu.git
synced 2025-03-28 09:37:23 +08:00
Merge branch 'master' into mod-overlay/settings-area
This commit is contained in:
commit
1acfbf490b
@ -27,7 +27,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ppy.localisationanalyser.tools": {
|
"ppy.localisationanalyser.tools": {
|
||||||
"version": "2021.1210.0",
|
"version": "2022.320.0",
|
||||||
"commands": [
|
"commands": [
|
||||||
"localisation"
|
"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:**
|
**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.
|
- 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.
|
||||||
|
@ -51,8 +51,8 @@
|
|||||||
<Reference Include="Java.Interop" />
|
<Reference Include="Java.Interop" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests
|
|||||||
{
|
{
|
||||||
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
|
public TestLegacySkin(SkinInfo skin, IResourceStore<byte[]> storage)
|
||||||
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
|
// Bypass LegacySkinResourceStore to avoid returning null for retrieving files due to bad skin info (SkinInfo.Files = null).
|
||||||
: base(skin, storage, null, "skin.ini")
|
: base(skin, null, storage)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override string Description => "No need to chase the circle – the circle chases you!";
|
public override string Description => "No need to chase the circle – the circle chases you!";
|
||||||
public override double ScoreMultiplier => 1;
|
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;
|
private IFrameStableClock gameplayClock;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
|
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.")]
|
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
|
||||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
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 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 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>
|
/// <summary>
|
||||||
/// How early before a hitobject's start time to trigger a hit.
|
/// 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()
|
public Slider()
|
||||||
{
|
{
|
||||||
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
|
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
|
||||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateNestedSamples();
|
UpdateNestedSamples();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNestedPositions()
|
private void updateNestedPositions()
|
||||||
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
TailCircle.Position = EndPosition;
|
TailCircle.Position = EndPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateNestedSamples()
|
protected void UpdateNestedSamples()
|
||||||
{
|
{
|
||||||
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
|
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)
|
?? 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 MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
|
||||||
new OsuModHidden(),
|
new OsuModHidden(),
|
||||||
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
|
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
|
||||||
|
new OsuModStrictTracking()
|
||||||
};
|
};
|
||||||
|
|
||||||
case ModType.Conversion:
|
case ModType.Conversion:
|
||||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
switch (osuComponent.Component)
|
switch (osuComponent.Component)
|
||||||
{
|
{
|
||||||
case OsuSkinComponents.FollowPoint:
|
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:
|
case OsuSkinComponents.SliderFollowCircle:
|
||||||
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||||
|
@ -175,7 +175,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
private class TestLegacySkin : LegacySkin
|
private class TestLegacySkin : LegacySkin
|
||||||
{
|
{
|
||||||
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
|
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
|
||||||
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
|
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, null, storage, fileName)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Replays;
|
using osu.Game.Replays;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
@ -64,6 +65,62 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(3, true)]
|
||||||
|
[TestCase(6, false)]
|
||||||
|
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)]
|
||||||
|
public void TestLegacyBeatmapReplayOffsetsDecode(int beatmapVersion, bool offsetApplied)
|
||||||
|
{
|
||||||
|
const double first_frame_time = 48;
|
||||||
|
const double second_frame_time = 65;
|
||||||
|
|
||||||
|
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
|
||||||
|
|
||||||
|
using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr"))
|
||||||
|
{
|
||||||
|
var score = decoder.Parse(resourceStream);
|
||||||
|
|
||||||
|
Assert.That(score.Replay.Frames[0].Time, Is.EqualTo(first_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
|
||||||
|
Assert.That(score.Replay.Frames[1].Time, Is.EqualTo(second_frame_time + (offsetApplied ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(3)]
|
||||||
|
[TestCase(6)]
|
||||||
|
[TestCase(LegacyBeatmapDecoder.LATEST_VERSION)]
|
||||||
|
public void TestLegacyBeatmapReplayOffsetsEncodeDecode(int beatmapVersion)
|
||||||
|
{
|
||||||
|
const double first_frame_time = 2000;
|
||||||
|
const double second_frame_time = 3000;
|
||||||
|
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||||
|
var beatmap = new TestBeatmap(ruleset)
|
||||||
|
{
|
||||||
|
BeatmapInfo =
|
||||||
|
{
|
||||||
|
BeatmapVersion = beatmapVersion
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var score = new Score
|
||||||
|
{
|
||||||
|
ScoreInfo = scoreInfo,
|
||||||
|
Replay = new Replay
|
||||||
|
{
|
||||||
|
Frames = new List<ReplayFrame>
|
||||||
|
{
|
||||||
|
new OsuReplayFrame(first_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton),
|
||||||
|
new OsuReplayFrame(second_frame_time, OsuPlayfield.BASE_SIZE / 2, OsuAction.LeftButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var decodedAfterEncode = encodeThenDecode(beatmapVersion, score, beatmap);
|
||||||
|
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[0].Time, Is.EqualTo(first_frame_time));
|
||||||
|
Assert.That(decodedAfterEncode.Replay.Frames[1].Time, Is.EqualTo(second_frame_time));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCultureInvariance()
|
public void TestCultureInvariance()
|
||||||
{
|
{
|
||||||
@ -86,15 +143,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
// rather than the classic ASCII U+002D HYPHEN-MINUS.
|
// rather than the classic ASCII U+002D HYPHEN-MINUS.
|
||||||
CultureInfo.CurrentCulture = new CultureInfo("se");
|
CultureInfo.CurrentCulture = new CultureInfo("se");
|
||||||
|
|
||||||
var encodeStream = new MemoryStream();
|
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||||
|
|
||||||
var encoder = new LegacyScoreEncoder(score, beatmap);
|
|
||||||
encoder.Encode(encodeStream);
|
|
||||||
|
|
||||||
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
|
||||||
|
|
||||||
var decoder = new TestLegacyScoreDecoder();
|
|
||||||
var decodedAfterEncode = decoder.Parse(decodeStream);
|
|
||||||
|
|
||||||
Assert.Multiple(() =>
|
Assert.Multiple(() =>
|
||||||
{
|
{
|
||||||
@ -110,6 +159,20 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||||
|
{
|
||||||
|
var encodeStream = new MemoryStream();
|
||||||
|
|
||||||
|
var encoder = new LegacyScoreEncoder(score, beatmap);
|
||||||
|
encoder.Encode(encodeStream);
|
||||||
|
|
||||||
|
var decodeStream = new MemoryStream(encodeStream.GetBuffer());
|
||||||
|
|
||||||
|
var decoder = new TestLegacyScoreDecoder(beatmapVersion);
|
||||||
|
var decodedAfterEncode = decoder.Parse(decodeStream);
|
||||||
|
return decodedAfterEncode;
|
||||||
|
}
|
||||||
|
|
||||||
[TearDown]
|
[TearDown]
|
||||||
public void TearDown()
|
public void TearDown()
|
||||||
{
|
{
|
||||||
@ -118,6 +181,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
|
|
||||||
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
private class TestLegacyScoreDecoder : LegacyScoreDecoder
|
||||||
{
|
{
|
||||||
|
private readonly int beatmapVersion;
|
||||||
|
|
||||||
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
|
private static readonly Dictionary<int, Ruleset> rulesets = new Ruleset[]
|
||||||
{
|
{
|
||||||
new OsuRuleset(),
|
new OsuRuleset(),
|
||||||
@ -126,6 +191,11 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
new ManiaRuleset()
|
new ManiaRuleset()
|
||||||
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
|
}.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID);
|
||||||
|
|
||||||
|
public TestLegacyScoreDecoder(int beatmapVersion = LegacyBeatmapDecoder.LATEST_VERSION)
|
||||||
|
{
|
||||||
|
this.beatmapVersion = beatmapVersion;
|
||||||
|
}
|
||||||
|
|
||||||
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
|
protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId];
|
||||||
|
|
||||||
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
|
protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap
|
||||||
@ -134,7 +204,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
MD5Hash = md5Hash,
|
MD5Hash = md5Hash,
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
Difficulty = new BeatmapDifficulty()
|
Difficulty = new BeatmapDifficulty(),
|
||||||
|
BeatmapVersion = beatmapVersion,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -147,7 +147,10 @@ namespace osu.Game.Tests.Database
|
|||||||
Live<BeatmapSetInfo>? imported;
|
Live<BeatmapSetInfo>? imported;
|
||||||
|
|
||||||
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
|
||||||
|
{
|
||||||
imported = await importer.Import(reader);
|
imported = await importer.Import(reader);
|
||||||
|
EnsureLoaded(realm.Realm);
|
||||||
|
}
|
||||||
|
|
||||||
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
|
Assert.AreEqual(1, realm.Realm.All<BeatmapSetInfo>().Count());
|
||||||
|
|
||||||
@ -510,6 +513,8 @@ namespace osu.Game.Tests.Database
|
|||||||
new ImportTask(zipStream, string.Empty)
|
new ImportTask(zipStream, string.Empty)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
checkBeatmapSetCount(realm.Realm, 0);
|
checkBeatmapSetCount(realm.Realm, 0);
|
||||||
checkBeatmapCount(realm.Realm, 0);
|
checkBeatmapCount(realm.Realm, 0);
|
||||||
|
|
||||||
@ -565,6 +570,8 @@ namespace osu.Game.Tests.Database
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnsureLoaded(realm.Realm);
|
||||||
|
|
||||||
checkBeatmapSetCount(realm.Realm, 1);
|
checkBeatmapSetCount(realm.Realm, 1);
|
||||||
checkBeatmapCount(realm.Realm, 12);
|
checkBeatmapCount(realm.Realm, 12);
|
||||||
|
|
||||||
@ -590,6 +597,8 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
Assert.IsTrue(imported.DeletePending);
|
Assert.IsTrue(imported.DeletePending);
|
||||||
|
|
||||||
|
var originalAddedDate = imported.DateAdded;
|
||||||
|
|
||||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||||
@ -597,6 +606,7 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||||
Assert.IsFalse(imported.DeletePending);
|
Assert.IsFalse(imported.DeletePending);
|
||||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||||
|
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,6 +656,8 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
Assert.IsTrue(imported.DeletePending);
|
Assert.IsTrue(imported.DeletePending);
|
||||||
|
|
||||||
|
var originalAddedDate = imported.DateAdded;
|
||||||
|
|
||||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||||
|
|
||||||
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
|
||||||
@ -653,6 +665,7 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
|
||||||
Assert.IsFalse(imported.DeletePending);
|
Assert.IsFalse(imported.DeletePending);
|
||||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||||
|
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -720,6 +733,8 @@ namespace osu.Game.Tests.Database
|
|||||||
|
|
||||||
var imported = importer.Import(toImport);
|
var imported = importer.Import(toImport);
|
||||||
|
|
||||||
|
realm.Run(r => r.Refresh());
|
||||||
|
|
||||||
Assert.NotNull(imported);
|
Assert.NotNull(imported);
|
||||||
Debug.Assert(imported != null);
|
Debug.Assert(imported != null);
|
||||||
|
|
||||||
@ -885,6 +900,8 @@ namespace osu.Game.Tests.Database
|
|||||||
string? temp = TestResources.GetTestBeatmapForImport();
|
string? temp = TestResources.GetTestBeatmapForImport();
|
||||||
await importer.Import(temp);
|
await importer.Import(temp);
|
||||||
|
|
||||||
|
EnsureLoaded(realm.Realm);
|
||||||
|
|
||||||
// Update via the beatmap, not the beatmap info, to ensure correct linking
|
// Update via the beatmap, not the beatmap info, to ensure correct linking
|
||||||
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();
|
BeatmapSetInfo setToUpdate = realm.Realm.All<BeatmapSetInfo>().First();
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
private class TestSkin : LegacySkin
|
private class TestSkin : LegacySkin
|
||||||
{
|
{
|
||||||
public TestSkin(string resourceName, IStorageResourceProvider resources)
|
public TestSkin(string resourceName, IStorageResourceProvider resources)
|
||||||
: base(DefaultLegacySkin.CreateInfo(), new TestResourceStore(resourceName), resources, "skin.ini")
|
: base(DefaultLegacySkin.CreateInfo(), resources, new TestResourceStore(resourceName))
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics.OpenGL.Textures;
|
using osu.Framework.Audio;
|
||||||
using osu.Framework.Graphics.Textures;
|
using osu.Framework.Graphics.Textures;
|
||||||
|
using osu.Framework.IO.Stores;
|
||||||
|
using osu.Game.Database;
|
||||||
|
using osu.Game.IO;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using SixLabors.ImageSharp;
|
||||||
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual.Skinning
|
namespace osu.Game.Tests.NonVisual.Skinning
|
||||||
{
|
{
|
||||||
@ -71,7 +80,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
|||||||
var texture = legacySkin.GetTexture(requestedComponent);
|
var texture = legacySkin.GetTexture(requestedComponent);
|
||||||
|
|
||||||
Assert.IsNotNull(texture);
|
Assert.IsNotNull(texture);
|
||||||
Assert.AreEqual(textureStore.Textures[expectedTexture], texture);
|
Assert.AreEqual(textureStore.Textures[expectedTexture].Width, texture.Width);
|
||||||
Assert.AreEqual(expectedScale, texture.ScaleAdjust);
|
Assert.AreEqual(expectedScale, texture.ScaleAdjust);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,23 +97,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
|||||||
|
|
||||||
private class TestLegacySkin : LegacySkin
|
private class TestLegacySkin : LegacySkin
|
||||||
{
|
{
|
||||||
public TestLegacySkin(TextureStore textureStore)
|
public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
|
||||||
: base(new SkinInfo(), null, null, string.Empty)
|
: base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
|
||||||
{
|
{
|
||||||
Textures = textureStore;
|
}
|
||||||
|
|
||||||
|
private class TestResourceProvider : IStorageResourceProvider
|
||||||
|
{
|
||||||
|
private readonly IResourceStore<TextureUpload> textureStore;
|
||||||
|
|
||||||
|
public TestResourceProvider(IResourceStore<TextureUpload> textureStore)
|
||||||
|
{
|
||||||
|
this.textureStore = textureStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioManager AudioManager => null;
|
||||||
|
public IResourceStore<byte[]> Files => null;
|
||||||
|
public IResourceStore<byte[]> Resources => null;
|
||||||
|
public RealmAccess RealmAccess => null;
|
||||||
|
public IResourceStore<TextureUpload> CreateTextureLoaderStore(IResourceStore<byte[]> underlyingStore) => textureStore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestTextureStore : TextureStore
|
private class TestTextureStore : IResourceStore<TextureUpload>
|
||||||
{
|
{
|
||||||
public readonly Dictionary<string, Texture> Textures;
|
public readonly Dictionary<string, TextureUpload> Textures;
|
||||||
|
|
||||||
public TestTextureStore(params string[] fileNames)
|
public TestTextureStore(params string[] fileNames)
|
||||||
{
|
{
|
||||||
Textures = fileNames.ToDictionary(fileName => fileName, fileName => new Texture(1, 1));
|
// use an incrementing width to allow assertion matching on correct textures as they turn from uploads into actual textures.
|
||||||
|
int width = 1;
|
||||||
|
Textures = fileNames.ToDictionary(fileName => fileName, fileName => new TextureUpload(new Image<Rgba32>(width, width++)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => Textures.GetValueOrDefault(name);
|
public TextureUpload Get(string name) => Textures.GetValueOrDefault(name);
|
||||||
|
|
||||||
|
public Task<TextureUpload> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => Task.FromResult(Get(name));
|
||||||
|
|
||||||
|
public Stream GetStream(string name) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAvailableResources() => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Skins
|
|||||||
public class BeatmapSkinSource : LegacyBeatmapSkin
|
public class BeatmapSkinSource : LegacyBeatmapSkin
|
||||||
{
|
{
|
||||||
public BeatmapSkinSource()
|
public BeatmapSkinSource()
|
||||||
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
|
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -202,7 +202,7 @@ namespace osu.Game.Tests.Skins
|
|||||||
public class BeatmapSkinSource : LegacyBeatmapSkin
|
public class BeatmapSkinSource : LegacyBeatmapSkin
|
||||||
{
|
{
|
||||||
public BeatmapSkinSource()
|
public BeatmapSkinSource()
|
||||||
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null, null)
|
: base(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, null)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using osu.Game.Overlays.Dialog;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
@ -23,6 +24,7 @@ using osu.Game.Screens.Edit.Setup;
|
|||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
using SharpCompress.Archives.Zip;
|
using SharpCompress.Archives.Zip;
|
||||||
|
|
||||||
@ -63,13 +65,19 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
EditorBeatmap editorBeatmap = null;
|
EditorBeatmap editorBeatmap = null;
|
||||||
|
|
||||||
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
|
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
|
||||||
AddStep("exit without save", () =>
|
|
||||||
|
AddStep("exit without save", () => Editor.Exit());
|
||||||
|
AddStep("hold to confirm", () =>
|
||||||
{
|
{
|
||||||
Editor.Exit();
|
var confirmButton = DialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogDangerousButton>().First();
|
||||||
DialogOverlay.CurrentDialog.PerformOkAction();
|
|
||||||
|
InputManager.MoveMouseTo(confirmButton);
|
||||||
|
InputManager.PressButton(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
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);
|
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
|
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]
|
[Test]
|
||||||
public void TestMetadata()
|
public void TestMetadata()
|
||||||
{
|
{
|
||||||
|
@ -18,9 +18,11 @@ using osu.Game.Rulesets;
|
|||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Play.HUD;
|
using osu.Game.Screens.Play.HUD;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
using osu.Game.Storyboards;
|
using osu.Game.Storyboards;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -37,12 +39,18 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
|
||||||
protected override bool HasCustomSteps => true;
|
protected override bool HasCustomSteps => true;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
public void TestEmptyLegacyBeatmapSkinFallsBack()
|
||||||
{
|
{
|
||||||
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null, null));
|
CreateSkinTest(DefaultSkin.CreateInfo(), () => new LegacyBeatmapSkin(new BeatmapInfo(), null));
|
||||||
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
AddUntilStep("wait for hud load", () => Player.ChildrenOfType<SkinnableTargetContainer>().All(c => c.ComponentsLoaded));
|
||||||
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
AddAssert("hud from default skin", () => AssertComponentsFromExpectedSource(SkinnableTarget.MainHUDComponents, skinManager.CurrentSkin.Value));
|
||||||
}
|
}
|
||||||
|
@ -8,12 +8,14 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -30,6 +32,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
|
||||||
// best way to check without exposing.
|
// best way to check without exposing.
|
||||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||||
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
||||||
|
@ -5,6 +5,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
AddStep("create player", () =>
|
AddStep("create player", () =>
|
||||||
{
|
{
|
||||||
Beatmap.Value = CreateWorkingBeatmap(beatmap, storyboard);
|
Beatmap.Value = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), Audio);
|
||||||
LoadScreen(player = new LeadInPlayer());
|
LoadScreen(player = new LeadInPlayer());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
using osu.Game.Skinning.Editor;
|
using osu.Game.Skinning.Editor;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
{
|
{
|
||||||
@ -29,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddStep("reload skin editor", () =>
|
AddStep("reload skin editor", () =>
|
||||||
{
|
{
|
||||||
skinEditor?.Expire();
|
skinEditor?.Expire();
|
||||||
Player.ScaleTo(0.8f);
|
Player.ScaleTo(0.4f);
|
||||||
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
|
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -40,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
|
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();
|
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,11 +5,13 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Skinning.Editor;
|
using osu.Game.Skinning.Editor;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -22,6 +24,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
|
@ -10,11 +10,13 @@ using osu.Framework.Extensions.IEnumerableExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Timing;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Gameplay
|
namespace osu.Game.Tests.Visual.Gameplay
|
||||||
@ -29,6 +31,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
[Cached(typeof(HealthProcessor))]
|
[Cached(typeof(HealthProcessor))]
|
||||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private GameplayState gameplayState = new GameplayState(new TestBeatmap(new OsuRuleset().RulesetInfo), new OsuRuleset());
|
||||||
|
|
||||||
|
[Cached]
|
||||||
|
private readonly GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||||
|
|
||||||
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
||||||
|
|
||||||
// best way to check without exposing.
|
// best way to check without exposing.
|
||||||
|
@ -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]
|
[Test]
|
||||||
public void TestFrameStarvationAndResume()
|
public void TestFrameStarvationAndResume()
|
||||||
{
|
{
|
||||||
@ -319,9 +369,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
private void checkPaused(bool state) =>
|
private void checkPaused(bool state) =>
|
||||||
AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType<DrawableRuleset>().First().IsPaused.Value == 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()
|
private void loadSpectatingScreen()
|
||||||
|
338
osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
Normal file
338
osu.Game.Tests/Visual/Multiplayer/TestSceneMatchStartControl.cs
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio;
|
||||||
|
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;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
using osu.Game.Online.Rooms;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Multiplayer
|
||||||
|
{
|
||||||
|
public class TestSceneMatchStartControl : MultiplayerTestScene
|
||||||
|
{
|
||||||
|
private MatchStartControl control;
|
||||||
|
private BeatmapSetInfo importedSet;
|
||||||
|
|
||||||
|
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private RulesetStore rulesets;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(GameHost host, AudioManager audio)
|
||||||
|
{
|
||||||
|
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||||
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||||
|
Dependencies.Cache(Realm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public new void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
||||||
|
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||||
|
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||||
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||||
|
|
||||||
|
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||||
|
{
|
||||||
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||||
|
};
|
||||||
|
|
||||||
|
Child = new PopoverContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = control = new MatchStartControl
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Size = new Vector2(200, 50),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStartWithCountdown()
|
||||||
|
{
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
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>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
|
||||||
|
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||||
|
AddUntilStep("match not started", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyAndUnReadyDuringCountdown()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("start with countdown", () => MultiplayerClient.SendMatchRequest(new StartMatchCountdownRequest { Duration = TimeSpan.FromMinutes(2) }).WaitSafely());
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCountdownButtonEnablementAndVisibilityWhileSpectating()
|
||||||
|
{
|
||||||
|
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<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<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||||
|
|
||||||
|
AddStep("set second user ready", () => MultiplayerClient.ChangeUserState(2, MultiplayerUserState.Ready));
|
||||||
|
AddAssert("countdown button enabled", () => this.ChildrenOfType<MultiplayerCountdownButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSpectatingDuringCountdownWithNoReadyUsersCancelsCountdown()
|
||||||
|
{
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set spectating", () => MultiplayerClient.ChangeUserState(API.LocalUser.Value.OnlineID, MultiplayerUserState.Spectating));
|
||||||
|
AddUntilStep("local user is spectating", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Spectating);
|
||||||
|
|
||||||
|
AddStep("finish countdown", () => MultiplayerClient.SkipToEndOfCountdown());
|
||||||
|
AddUntilStep("match not started", () => MultiplayerClient.Room?.State == MultiplayerRoomState.Open);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestReadyButtonEnabledWhileSpectatingDuringCountdown()
|
||||||
|
{
|
||||||
|
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>();
|
||||||
|
AddUntilStep("countdown button shown", () => this.ChildrenOfType<MultiplayerCountdownButton>().SingleOrDefault()?.IsPresent == true);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerCountdownButton>();
|
||||||
|
AddStep("click the first countdown button", () =>
|
||||||
|
{
|
||||||
|
var popoverButton = this.ChildrenOfType<Popover>().Single().ChildrenOfType<OsuButton>().First();
|
||||||
|
InputManager.MoveMouseTo(popoverButton);
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("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>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBecomeHostDuringCountdownAndReady()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
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>();
|
||||||
|
AddUntilStep("local user became ready", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready);
|
||||||
|
AddAssert("countdown still active", () => MultiplayerClient.Room?.Countdown != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDeletedBeatmapDisableReady()
|
||||||
|
{
|
||||||
|
OsuButton readyButton = null;
|
||||||
|
|
||||||
|
AddUntilStep("ensure ready button enabled", () =>
|
||||||
|
{
|
||||||
|
readyButton = control.ChildrenOfType<OsuButton>().Single();
|
||||||
|
return readyButton.Enabled.Value;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||||
|
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
|
||||||
|
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
||||||
|
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestToggleStateWhenNotHost()
|
||||||
|
{
|
||||||
|
AddStep("add second user as host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestToggleStateWhenHost(bool allReady)
|
||||||
|
{
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
|
||||||
|
if (!allReady)
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
|
||||||
|
verifyGameplayStartFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBecomeHostWhileReady()
|
||||||
|
{
|
||||||
|
AddStep("add host", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
MultiplayerClient.TransferHost(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
||||||
|
|
||||||
|
verifyGameplayStartFlow();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestLoseHostWhileReady()
|
||||||
|
{
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
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>();
|
||||||
|
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
||||||
|
AddUntilStep("ready button enabled", () => control.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(true)]
|
||||||
|
[TestCase(false)]
|
||||||
|
public void TestManyUsersChangingState(bool isHost)
|
||||||
|
{
|
||||||
|
const int users = 10;
|
||||||
|
AddStep("setup", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
||||||
|
for (int i = 0; i < users; i++)
|
||||||
|
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isHost)
|
||||||
|
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
|
||||||
|
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
|
||||||
|
AddRepeatStep("change user ready state", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
|
||||||
|
}, 20);
|
||||||
|
|
||||||
|
AddRepeatStep("ready all users", () =>
|
||||||
|
{
|
||||||
|
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
|
||||||
|
if (nextUnready != null)
|
||||||
|
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
||||||
|
}, users);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyGameplayStartFlow()
|
||||||
|
{
|
||||||
|
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
||||||
|
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
||||||
|
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
||||||
|
|
||||||
|
AddStep("finish gameplay", () =>
|
||||||
|
{
|
||||||
|
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
|
||||||
|
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
||||||
|
});
|
||||||
|
|
||||||
|
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.Screens.Spectate;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
using ReadyButton = osu.Game.Screens.OnlinePlay.Components.ReadyButton;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
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);
|
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);
|
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);
|
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);
|
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)));
|
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);
|
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);
|
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());
|
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);
|
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", () =>
|
AddStep("restore beatmap", () =>
|
||||||
{
|
{
|
||||||
@ -883,7 +884,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("start match by other user", () =>
|
AddStep("start match by other user", () =>
|
||||||
{
|
{
|
||||||
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
multiplayerClient.ChangeUserState(1234, MultiplayerUserState.Ready);
|
||||||
multiplayerClient.StartMatch();
|
multiplayerClient.StartMatch().WaitSafely();
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
AddUntilStep("wait for loading", () => multiplayerClient.Room?.State == MultiplayerRoomState.WaitingForLoad);
|
||||||
|
@ -163,6 +163,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
AddUntilStep("second user crown visible", () => this.ChildrenOfType<ParticipantPanel>().ElementAt(1).ChildrenOfType<SpriteIcon>().First().Alpha == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHostGetsPinnedToTop()
|
||||||
|
{
|
||||||
|
AddStep("add user", () => MultiplayerClient.AddUser(new APIUser
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
Username = "Second",
|
||||||
|
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
|
||||||
|
AddAssert("second user above first", () =>
|
||||||
|
{
|
||||||
|
var first = this.ChildrenOfType<ParticipantPanel>().ElementAt(0);
|
||||||
|
var second = this.ChildrenOfType<ParticipantPanel>().ElementAt(1);
|
||||||
|
return second.Y < first.Y;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestKickButtonOnlyPresentWhenHost()
|
public void TestKickButtonOnlyPresentWhenHost()
|
||||||
{
|
{
|
||||||
@ -202,9 +221,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestManyUsers()
|
public void TestManyUsers()
|
||||||
{
|
{
|
||||||
|
const int users_count = 20;
|
||||||
|
|
||||||
AddStep("add many users", () =>
|
AddStep("add many users", () =>
|
||||||
{
|
{
|
||||||
for (int i = 0; i < 20; i++)
|
for (int i = 0; i < users_count; i++)
|
||||||
{
|
{
|
||||||
MultiplayerClient.AddUser(new APIUser
|
MultiplayerClient.AddUser(new APIUser
|
||||||
{
|
{
|
||||||
@ -243,6 +264,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddRepeatStep("switch hosts", () => MultiplayerClient.TransferHost(RNG.Next(0, users_count)), 10);
|
||||||
|
AddStep("give host back", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -11,6 +11,8 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -129,6 +131,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("item 1 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
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.")]
|
[Ignore("Expired items are initially removed from the room.")]
|
||||||
[Test]
|
[Test]
|
||||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
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 void changeDisplayModeStep(MultiplayerPlaylistDisplayMode mode) => AddStep($"change list to {mode}", () => list.DisplayMode.Value = mode);
|
||||||
|
|
||||||
private bool inQueueList(int playlistItemId)
|
private bool inQueueList(int playlistItemId)
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NUnit.Framework;
|
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Audio;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Extensions;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Platform;
|
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Beatmaps;
|
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
|
||||||
using osu.Game.Online.Rooms;
|
|
||||||
using osu.Game.Rulesets;
|
|
||||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
|
|
||||||
using osu.Game.Tests.Resources;
|
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Multiplayer
|
|
||||||
{
|
|
||||||
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
|
|
||||||
{
|
|
||||||
private MultiplayerReadyButton button;
|
|
||||||
private BeatmapSetInfo importedSet;
|
|
||||||
|
|
||||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
|
||||||
|
|
||||||
private BeatmapManager beatmaps;
|
|
||||||
private RulesetStore rulesets;
|
|
||||||
|
|
||||||
private IDisposable readyClickOperation;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
|
||||||
private void load(GameHost host, AudioManager audio)
|
|
||||||
{
|
|
||||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
|
||||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
|
||||||
Dependencies.Cache(Realm);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SetUp]
|
|
||||||
public new void Setup() => Schedule(() =>
|
|
||||||
{
|
|
||||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
|
||||||
|
|
||||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
|
||||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
|
||||||
|
|
||||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
|
||||||
{
|
|
||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
|
||||||
};
|
|
||||||
|
|
||||||
if (button != null)
|
|
||||||
Remove(button);
|
|
||||||
|
|
||||||
Add(button = new MultiplayerReadyButton
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(200, 50),
|
|
||||||
OnReadyClick = () =>
|
|
||||||
{
|
|
||||||
readyClickOperation = OngoingOperationTracker.BeginOperation();
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
|
||||||
{
|
|
||||||
if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
|
||||||
{
|
|
||||||
await MultiplayerClient.StartMatch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MultiplayerClient.ToggleReady();
|
|
||||||
|
|
||||||
readyClickOperation.Dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestDeletedBeatmapDisableReady()
|
|
||||||
{
|
|
||||||
OsuButton readyButton = null;
|
|
||||||
|
|
||||||
AddUntilStep("ensure ready button enabled", () =>
|
|
||||||
{
|
|
||||||
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
|
||||||
return readyButton.Enabled.Value;
|
|
||||||
});
|
|
||||||
|
|
||||||
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
|
||||||
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
|
|
||||||
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
|
||||||
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestToggleStateWhenNotHost()
|
|
||||||
{
|
|
||||||
AddStep("add second user as host", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
MultiplayerClient.TransferHost(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is idle", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(true)]
|
|
||||||
[TestCase(false)]
|
|
||||||
public void TestToggleStateWhenHost(bool allReady)
|
|
||||||
{
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
|
|
||||||
if (!allReady)
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
|
|
||||||
verifyGameplayStartFlow();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestBecomeHostWhileReady()
|
|
||||||
{
|
|
||||||
AddStep("add host", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
MultiplayerClient.TransferHost(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddStep("make user host", () => MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0));
|
|
||||||
|
|
||||||
verifyGameplayStartFlow();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestLoseHostWhileReady()
|
|
||||||
{
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = 2, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
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>();
|
|
||||||
AddUntilStep("user is idle (match not started)", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Idle);
|
|
||||||
AddAssert("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[TestCase(true)]
|
|
||||||
[TestCase(false)]
|
|
||||||
public void TestManyUsersChangingState(bool isHost)
|
|
||||||
{
|
|
||||||
const int users = 10;
|
|
||||||
AddStep("setup", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.TransferHost(MultiplayerClient.Room?.Users[0].UserID ?? 0);
|
|
||||||
for (int i = 0; i < users; i++)
|
|
||||||
MultiplayerClient.AddUser(new APIUser { Id = i, Username = "Another user" });
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isHost)
|
|
||||||
AddStep("transfer host", () => MultiplayerClient.TransferHost(2));
|
|
||||||
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
|
|
||||||
AddRepeatStep("change user ready state", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle);
|
|
||||||
}, 20);
|
|
||||||
|
|
||||||
AddRepeatStep("ready all users", () =>
|
|
||||||
{
|
|
||||||
var nextUnready = MultiplayerClient.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle);
|
|
||||||
if (nextUnready != null)
|
|
||||||
MultiplayerClient.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready);
|
|
||||||
}, users);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void verifyGameplayStartFlow()
|
|
||||||
{
|
|
||||||
AddUntilStep("user is ready", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.Ready);
|
|
||||||
ClickButtonWhenEnabled<MultiplayerReadyButton>();
|
|
||||||
AddUntilStep("user waiting for load", () => MultiplayerClient.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad);
|
|
||||||
|
|
||||||
AddAssert("ready button disabled", () => !button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
AddStep("transitioned to gameplay", () => readyClickOperation.Dispose());
|
|
||||||
|
|
||||||
AddStep("finish gameplay", () =>
|
|
||||||
{
|
|
||||||
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.Loaded);
|
|
||||||
MultiplayerClient.ChangeUserState(MultiplayerClient.Room?.Users[0].UserID ?? 0, MultiplayerUserState.FinishedPlay);
|
|
||||||
});
|
|
||||||
|
|
||||||
AddUntilStep("ready button enabled", () => button.ChildrenOfType<OsuButton>().Single().Enabled.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,9 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Audio;
|
||||||
@ -11,6 +9,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -28,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
public class TestSceneMultiplayerSpectateButton : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
private MultiplayerSpectateButton spectateButton;
|
private MultiplayerSpectateButton spectateButton;
|
||||||
private MultiplayerReadyButton readyButton;
|
private MatchStartControl startControl;
|
||||||
|
|
||||||
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
|
||||||
|
|
||||||
@ -36,8 +35,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
private BeatmapManager beatmaps;
|
private BeatmapManager beatmaps;
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets;
|
||||||
|
|
||||||
private IDisposable readyClickOperation;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
{
|
{
|
||||||
@ -60,49 +57,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID,
|
||||||
};
|
};
|
||||||
|
|
||||||
Child = new FillFlowContainer
|
Child = new PopoverContainer
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Direction = FillDirection.Vertical,
|
Child = new FillFlowContainer
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
spectateButton = new MultiplayerSpectateButton
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
spectateButton = new MultiplayerSpectateButton
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(200, 50),
|
|
||||||
OnSpectateClick = () =>
|
|
||||||
{
|
{
|
||||||
readyClickOperation = OngoingOperationTracker.BeginOperation();
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
Task.Run(async () =>
|
Size = new Vector2(200, 50),
|
||||||
{
|
},
|
||||||
await MultiplayerClient.ToggleSpectate();
|
startControl = new MatchStartControl
|
||||||
readyClickOperation.Dispose();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
readyButton = new MultiplayerReadyButton
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
Size = new Vector2(200, 50),
|
|
||||||
OnReadyClick = () =>
|
|
||||||
{
|
{
|
||||||
readyClickOperation = OngoingOperationTracker.BeginOperation();
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
Task.Run(async () =>
|
Size = new Vector2(200, 50),
|
||||||
{
|
|
||||||
if (MultiplayerClient.IsHost && MultiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
|
||||||
{
|
|
||||||
await MultiplayerClient.StartMatch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await MultiplayerClient.ToggleReady();
|
|
||||||
|
|
||||||
readyClickOperation.Dispose();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,6 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
=> AddUntilStep($"spectate button {(shouldBeEnabled ? "is" : "is not")} enabled", () => spectateButton.ChildrenOfType<OsuButton>().Single().Enabled.Value == shouldBeEnabled);
|
||||||
|
|
||||||
private void assertReadyButtonEnablement(bool 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.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
|
|||||||
using osu.Game.Online.Leaderboards;
|
using osu.Game.Online.Leaderboards;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Mods;
|
using osu.Game.Overlays.Mods;
|
||||||
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
@ -21,10 +23,12 @@ using osu.Game.Scoring;
|
|||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
using osu.Game.Screens.Select.Leaderboards;
|
using osu.Game.Screens.Select.Leaderboards;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
|
using osu.Game.Skinning.Editor;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
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]
|
[Test]
|
||||||
public void TestRetryCountIncrements()
|
public void TestRetryCountIncrements()
|
||||||
{
|
{
|
||||||
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
|
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);
|
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("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);
|
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();
|
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]
|
[Test]
|
||||||
public void TestExitMultiWithEscape()
|
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 });
|
||||||
|
}
|
||||||
|
}
|
@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestHideOverlay()
|
public void TestHideOverlay()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
|
|
||||||
AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
|
||||||
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
||||||
|
|
||||||
@ -134,6 +136,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChannelSelection()
|
public void TestChannelSelection()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
|
||||||
AddStep("Setup get message response", () => onGetMessages = channel =>
|
AddStep("Setup get message response", () => onGetMessages = channel =>
|
||||||
{
|
{
|
||||||
@ -169,6 +172,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestSearchInSelector()
|
public void TestSearchInSelector()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType<SearchTextBox>().First().Text = "no. 2");
|
AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType<SearchTextBox>().First().Text = "no. 2");
|
||||||
AddUntilStep("Only channel 2 visible", () =>
|
AddUntilStep("Only channel 2 visible", () =>
|
||||||
{
|
{
|
||||||
@ -180,6 +184,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChannelShortcutKeys()
|
public void TestChannelShortcutKeys()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel)));
|
AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel)));
|
||||||
AddStep("Close channel selector", () => InputManager.Key(Key.Escape));
|
AddStep("Close channel selector", () => InputManager.Key(Key.Escape));
|
||||||
AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
|
AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
|
||||||
@ -199,6 +204,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCloseChannelBehaviour()
|
public void TestCloseChannelBehaviour()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddUntilStep("Join until dropdown has channels", () =>
|
AddUntilStep("Join until dropdown has channels", () =>
|
||||||
{
|
{
|
||||||
if (visibleChannels.Count() < joinedChannels.Count())
|
if (visibleChannels.Count() < joinedChannels.Count())
|
||||||
@ -269,6 +275,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChannelCloseButton()
|
public void TestChannelCloseButton()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join 2 channels", () =>
|
AddStep("Join 2 channels", () =>
|
||||||
{
|
{
|
||||||
channelManager.JoinChannel(channel1);
|
channelManager.JoinChannel(channel1);
|
||||||
@ -289,6 +296,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCloseTabShortcut()
|
public void TestCloseTabShortcut()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join 2 channels", () =>
|
AddStep("Join 2 channels", () =>
|
||||||
{
|
{
|
||||||
channelManager.JoinChannel(channel1);
|
channelManager.JoinChannel(channel1);
|
||||||
@ -314,6 +322,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNewTabShortcut()
|
public void TestNewTabShortcut()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join 2 channels", () =>
|
AddStep("Join 2 channels", () =>
|
||||||
{
|
{
|
||||||
channelManager.JoinChannel(channel1);
|
channelManager.JoinChannel(channel1);
|
||||||
@ -330,6 +339,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRestoreTabShortcut()
|
public void TestRestoreTabShortcut()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join 3 channels", () =>
|
AddStep("Join 3 channels", () =>
|
||||||
{
|
{
|
||||||
channelManager.JoinChannel(channel1);
|
channelManager.JoinChannel(channel1);
|
||||||
@ -375,6 +385,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestChatCommand()
|
public void TestChatCommand()
|
||||||
{
|
{
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
|
||||||
@ -398,6 +409,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
Channel multiplayerChannel = null;
|
Channel multiplayerChannel = null;
|
||||||
|
|
||||||
|
AddStep("open chat overlay", () => chatOverlay.Show());
|
||||||
|
|
||||||
AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
|
AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
|
||||||
{
|
{
|
||||||
Name = "#mp_1",
|
Name = "#mp_1",
|
||||||
@ -417,6 +430,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
Message message = null;
|
Message message = null;
|
||||||
|
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
|
||||||
@ -443,6 +457,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
Message message = null;
|
Message message = null;
|
||||||
|
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
|
||||||
@ -471,6 +486,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
Message message = null;
|
Message message = null;
|
||||||
|
|
||||||
|
AddStep("Open chat overlay", () => chatOverlay.Show());
|
||||||
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
||||||
|
|
||||||
@ -496,14 +513,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHighlightWhileChatHidden()
|
public void TestHighlightWhileChatNeverOpen()
|
||||||
{
|
{
|
||||||
Message message = null;
|
Message message = null;
|
||||||
|
|
||||||
AddStep("hide chat", () => chatOverlay.Hide());
|
|
||||||
|
|
||||||
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
|
||||||
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
|
|
||||||
|
|
||||||
AddStep("Send message in channel 1", () =>
|
AddStep("Send message in channel 1", () =>
|
||||||
{
|
{
|
||||||
@ -520,7 +534,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Highlight message and show chat", () =>
|
AddStep("Highlight message and open chat", () =>
|
||||||
{
|
{
|
||||||
chatOverlay.HighlightMessage(message, channel1);
|
chatOverlay.HighlightMessage(message, channel1);
|
||||||
chatOverlay.Show();
|
chatOverlay.Show();
|
||||||
@ -571,8 +585,6 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
ChannelManager,
|
ChannelManager,
|
||||||
ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
|
ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
|
||||||
};
|
};
|
||||||
|
|
||||||
ChatOverlay.Show();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,6 +40,10 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
Text = @"You're a fake!",
|
Text = @"You're a fake!",
|
||||||
},
|
},
|
||||||
|
new PopupDialogDangerousButton
|
||||||
|
{
|
||||||
|
Text = @"Careful with this one..",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Tournament.Screens.Setup
|
|||||||
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
|
dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true);
|
||||||
|
|
||||||
Action = () => game.GracefullyExit();
|
Action = () => game.GracefullyExit();
|
||||||
folderButton.Action = storage.PresentExternally;
|
folderButton.Action = () => storage.PresentExternally();
|
||||||
|
|
||||||
ButtonText = "Close osu!";
|
ButtonText = "Close osu!";
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using Realms;
|
using Realms;
|
||||||
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
|
|||||||
[Ignored]
|
[Ignored]
|
||||||
public APIBeatmap? OnlineInfo { get; set; }
|
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]
|
[Ignored]
|
||||||
|
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
|
||||||
public int? MaxCombo { get; set; }
|
public int? MaxCombo { get; set; }
|
||||||
|
|
||||||
[Ignored]
|
[Ignored]
|
||||||
|
@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
|
|||||||
[NotMapped]
|
[NotMapped]
|
||||||
public APIBeatmap OnlineInfo { get; set; }
|
public APIBeatmap OnlineInfo { get; set; }
|
||||||
|
|
||||||
[NotMapped]
|
|
||||||
public int? MaxCombo { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The playable length in milliseconds of this beatmap.
|
/// The playable length in milliseconds of this beatmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
|
public class LegacyBeatmapDecoder : LegacyDecoder<Beatmap>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An offset which needs to be applied to old beatmaps (v4 and lower) to correct timing changes that were applied at a game client level.
|
||||||
|
/// </summary>
|
||||||
|
public const int EARLY_VERSION_TIMING_OFFSET = 24;
|
||||||
|
|
||||||
internal static RulesetStore RulesetStore;
|
internal static RulesetStore RulesetStore;
|
||||||
|
|
||||||
private Beatmap beatmap;
|
private Beatmap beatmap;
|
||||||
@ -50,8 +55,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
RulesetStore = new AssemblyRulesetStore();
|
RulesetStore = new AssemblyRulesetStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
// BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off)
|
offset = FormatVersion < 5 ? EARLY_VERSION_TIMING_OFFSET : 0;
|
||||||
offset = FormatVersion < 5 ? 24 : 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Beatmap CreateTemplateObject()
|
protected override Beatmap CreateTemplateObject()
|
||||||
|
@ -225,7 +225,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources);
|
return new LegacyBeatmapSkin(BeatmapInfo, resources);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
@ -295,7 +295,6 @@ namespace osu.Game.Database
|
|||||||
TimelineZoom = beatmap.TimelineZoom,
|
TimelineZoom = beatmap.TimelineZoom,
|
||||||
Countdown = beatmap.Countdown,
|
Countdown = beatmap.Countdown,
|
||||||
CountdownOffset = beatmap.CountdownOffset,
|
CountdownOffset = beatmap.CountdownOffset,
|
||||||
MaxCombo = beatmap.MaxCombo,
|
|
||||||
Bookmarks = beatmap.Bookmarks,
|
Bookmarks = beatmap.Bookmarks,
|
||||||
BeatmapSet = realmBeatmapSet,
|
BeatmapSet = realmBeatmapSet,
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
@ -17,6 +19,7 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Statistics;
|
using osu.Framework.Statistics;
|
||||||
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
@ -28,8 +31,6 @@ using osu.Game.Stores;
|
|||||||
using Realms;
|
using Realms;
|
||||||
using Realms.Exceptions;
|
using Realms.Exceptions;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -46,6 +47,8 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
private readonly IDatabaseContextFactory? efContextFactory;
|
private readonly IDatabaseContextFactory? efContextFactory;
|
||||||
|
|
||||||
|
private readonly SynchronizationContext? updateThreadSyncContext;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Version history:
|
/// Version history:
|
||||||
/// 6 ~2021-10-18 First tracked version.
|
/// 6 ~2021-10-18 First tracked version.
|
||||||
@ -143,12 +146,15 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
|
/// <param name="storage">The game storage which will be used to create the realm backing file.</param>
|
||||||
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
|
/// <param name="filename">The filename to use for the realm backing file. A ".realm" extension will be added automatically if not specified.</param>
|
||||||
|
/// <param name="updateThread">The game update thread, used to post realm operations into a thread-safe context.</param>
|
||||||
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
|
/// <param name="efContextFactory">An EF factory used only for migration purposes.</param>
|
||||||
public RealmAccess(Storage storage, string filename, IDatabaseContextFactory? efContextFactory = null)
|
public RealmAccess(Storage storage, string filename, GameThread? updateThread = null, IDatabaseContextFactory? efContextFactory = null)
|
||||||
{
|
{
|
||||||
this.storage = storage;
|
this.storage = storage;
|
||||||
this.efContextFactory = efContextFactory;
|
this.efContextFactory = efContextFactory;
|
||||||
|
|
||||||
|
updateThreadSyncContext = updateThread?.SynchronizationContext ?? SynchronizationContext.Current;
|
||||||
|
|
||||||
Filename = filename;
|
Filename = filename;
|
||||||
|
|
||||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||||
@ -379,9 +385,6 @@ namespace osu.Game.Database
|
|||||||
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
|
public IDisposable RegisterForNotifications<T>(Func<Realm, IQueryable<T>> query, NotificationCallbackDelegate<T> callback)
|
||||||
where T : RealmObjectBase
|
where T : RealmObjectBase
|
||||||
{
|
{
|
||||||
if (!ThreadSafety.IsUpdateThread)
|
|
||||||
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
|
|
||||||
|
|
||||||
lock (realmLock)
|
lock (realmLock)
|
||||||
{
|
{
|
||||||
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
|
Func<Realm, IDisposable?> action = realm => query(realm).QueryAsyncWithNotifications(callback);
|
||||||
@ -459,23 +462,24 @@ namespace osu.Game.Database
|
|||||||
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
|
/// <returns>An <see cref="IDisposable"/> which should be disposed to unsubscribe any inner subscription.</returns>
|
||||||
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
|
public IDisposable RegisterCustomSubscription(Func<Realm, IDisposable?> action)
|
||||||
{
|
{
|
||||||
if (!ThreadSafety.IsUpdateThread)
|
if (updateThreadSyncContext == null)
|
||||||
throw new InvalidOperationException(@$"{nameof(RegisterForNotifications)} must be called from the update thread.");
|
throw new InvalidOperationException("Attempted to register a realm subscription before update thread registration.");
|
||||||
|
|
||||||
var syncContext = SynchronizationContext.Current;
|
|
||||||
|
|
||||||
total_subscriptions.Value++;
|
total_subscriptions.Value++;
|
||||||
|
|
||||||
registerSubscription(action);
|
if (ThreadSafety.IsUpdateThread)
|
||||||
|
updateThreadSyncContext.Send(_ => registerSubscription(action), null);
|
||||||
|
else
|
||||||
|
updateThreadSyncContext.Post(_ => registerSubscription(action), null);
|
||||||
|
|
||||||
// This token is returned to the consumer.
|
// This token is returned to the consumer.
|
||||||
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
|
// When disposed, it will cause the registration to be permanently ceased (unsubscribed with realm and unregistered by this class).
|
||||||
return new InvokeOnDisposal(() =>
|
return new InvokeOnDisposal(() =>
|
||||||
{
|
{
|
||||||
if (ThreadSafety.IsUpdateThread)
|
if (ThreadSafety.IsUpdateThread)
|
||||||
syncContext.Send(_ => unsubscribe(), null);
|
updateThreadSyncContext.Send(_ => unsubscribe(), null);
|
||||||
else
|
else
|
||||||
syncContext.Post(_ => unsubscribe(), null);
|
updateThreadSyncContext.Post(_ => unsubscribe(), null);
|
||||||
|
|
||||||
void unsubscribe()
|
void unsubscribe()
|
||||||
{
|
{
|
||||||
|
@ -28,6 +28,14 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual bool AllowMultipleFires => false;
|
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();
|
public Bindable<double> Progress = new BindableDouble();
|
||||||
|
|
||||||
private Bindable<double> holdActivationDelay;
|
private Bindable<double> holdActivationDelay;
|
||||||
@ -35,7 +43,9 @@ namespace osu.Game.Graphics.Containers
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OsuConfigManager config)
|
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()
|
protected void BeginConfirm()
|
||||||
|
@ -45,8 +45,9 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected readonly Container ColourContainer;
|
||||||
|
|
||||||
private readonly Container backgroundContainer;
|
private readonly Container backgroundContainer;
|
||||||
private readonly Container colourContainer;
|
|
||||||
private readonly Container glowContainer;
|
private readonly Container glowContainer;
|
||||||
private readonly Box leftGlow;
|
private readonly Box leftGlow;
|
||||||
private readonly Box centerGlow;
|
private readonly Box centerGlow;
|
||||||
@ -113,7 +114,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Masking = true,
|
Masking = true,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
colourContainer = new Container
|
ColourContainer = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -182,7 +183,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
buttonColour = value;
|
buttonColour = value;
|
||||||
updateGlow();
|
updateGlow();
|
||||||
colourContainer.Colour = value;
|
ColourContainer.Colour = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,11 +231,11 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
Alpha = 0.05f
|
Alpha = 0.05f
|
||||||
};
|
};
|
||||||
|
|
||||||
colourContainer.Add(flash);
|
ColourContainer.Add(flash);
|
||||||
flash.FadeOutFromOne(100).Expire();
|
flash.FadeOutFromOne(100).Expire();
|
||||||
|
|
||||||
clickAnimating = true;
|
clickAnimating = true;
|
||||||
colourContainer.ResizeWidthTo(colourContainer.Width * 1.05f, 100, Easing.OutQuint)
|
ColourContainer.ResizeWidthTo(ColourContainer.Width * 1.05f, 100, Easing.OutQuint)
|
||||||
.OnComplete(_ =>
|
.OnComplete(_ =>
|
||||||
{
|
{
|
||||||
clickAnimating = false;
|
clickAnimating = false;
|
||||||
@ -246,14 +247,14 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
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);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
{
|
{
|
||||||
if (State == SelectionState.Selected)
|
if (State == SelectionState.Selected)
|
||||||
colourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
|
ColourContainer.ResizeWidthTo(hover_width, click_duration, Easing.In);
|
||||||
base.OnMouseUp(e);
|
base.OnMouseUp(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -279,12 +280,12 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
if (newState == SelectionState.Selected)
|
if (newState == SelectionState.Selected)
|
||||||
{
|
{
|
||||||
spriteText.TransformSpacingTo(hoverSpacing, hover_duration, Easing.OutElastic);
|
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);
|
glowContainer.FadeIn(hover_duration, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
else
|
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);
|
spriteText.TransformSpacingTo(Vector2.Zero, hover_duration, Easing.OutElastic);
|
||||||
glowContainer.FadeOut(hover_duration, Easing.OutQuint);
|
glowContainer.FadeOut(hover_duration, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
|
@ -70,9 +70,9 @@ namespace osu.Game.IO
|
|||||||
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
public override Stream GetStream(string path, FileAccess access = FileAccess.Read, FileMode mode = FileMode.OpenOrCreate) =>
|
||||||
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
UnderlyingStorage.GetStream(MutatePath(path), access, mode);
|
||||||
|
|
||||||
public override void OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
|
public override bool OpenFileExternally(string filename) => UnderlyingStorage.OpenFileExternally(MutatePath(filename));
|
||||||
|
|
||||||
public override void PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
|
public override bool PresentFileExternally(string filename) => UnderlyingStorage.PresentFileExternally(MutatePath(filename));
|
||||||
|
|
||||||
public override Storage GetStorageForDirectory(string path)
|
public override Storage GetStorageForDirectory(string path)
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,22 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates a change to the <see cref="MultiplayerRoom"/>'s countdown.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class CountdownChangedEvent : MatchServerEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The new countdown.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public MultiplayerCountdown? Countdown { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A request for a countdown to start the match.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class StartMatchCountdownRequest : MatchUserRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// How long the countdown should last.
|
||||||
|
/// </summary>
|
||||||
|
[Key(0)]
|
||||||
|
public TimeSpan Duration { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer.Countdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Request to stop the current countdown.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class StopCountdownRequest : MatchUserRequest
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,11 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
{
|
{
|
||||||
@ -11,6 +14,8 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
|
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
[Union(0, typeof(CountdownChangedEvent))]
|
||||||
public abstract class MatchServerEvent
|
public abstract class MatchServerEvent
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
17
osu.Game/Online/Multiplayer/MatchStartCountdown.cs
Normal file
17
osu.Game/Online/Multiplayer/MatchStartCountdown.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="MultiplayerCountdown"/> which will start the match after ending.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
public class MatchStartCountdown : MultiplayerCountdown
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ using MessagePack;
|
|||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
namespace osu.Game.Online.Multiplayer.MatchTypes.TeamVersus
|
||||||
{
|
{
|
||||||
|
[MessagePackObject]
|
||||||
public class ChangeTeamRequest : MatchUserRequest
|
public class ChangeTeamRequest : MatchUserRequest
|
||||||
{
|
{
|
||||||
[Key(0)]
|
[Key(0)]
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using MessagePack;
|
using MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
|
|
||||||
namespace osu.Game.Online.Multiplayer
|
namespace osu.Game.Online.Multiplayer
|
||||||
@ -12,7 +13,10 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[MessagePackObject]
|
[MessagePackObject]
|
||||||
[Union(0, typeof(ChangeTeamRequest))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
// IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
[Union(0, typeof(ChangeTeamRequest))]
|
||||||
|
[Union(1, typeof(StartMatchCountdownRequest))]
|
||||||
|
[Union(2, typeof(StopCountdownRequest))]
|
||||||
public abstract class MatchUserRequest
|
public abstract class MatchUserRequest
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ using osu.Framework.Logging;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Online.Rooms.RoomStatuses;
|
using osu.Game.Online.Rooms.RoomStatuses;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -170,6 +171,8 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
Room = joinedRoom;
|
Room = joinedRoom;
|
||||||
APIRoom = room;
|
APIRoom = room;
|
||||||
|
|
||||||
|
Debug.Assert(joinedRoom.Playlist.Count > 0);
|
||||||
|
|
||||||
APIRoom.Playlist.Clear();
|
APIRoom.Playlist.Clear();
|
||||||
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
|
APIRoom.Playlist.AddRange(joinedRoom.Playlist.Select(createPlaylistItem));
|
||||||
|
|
||||||
@ -534,7 +537,24 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
|
|
||||||
public Task MatchEvent(MatchServerEvent e)
|
public Task MatchEvent(MatchServerEvent e)
|
||||||
{
|
{
|
||||||
// not used by any match types just yet.
|
if (Room == null)
|
||||||
|
return Task.CompletedTask;
|
||||||
|
|
||||||
|
Scheduler.Add(() =>
|
||||||
|
{
|
||||||
|
if (Room == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (e)
|
||||||
|
{
|
||||||
|
case CountdownChangedEvent countdownChangedEvent:
|
||||||
|
Room.Countdown = countdownChangedEvent.Countdown;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomUpdated?.Invoke();
|
||||||
|
}, false);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -665,6 +685,8 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
|
Room.Playlist.Remove(Room.Playlist.Single(existing => existing.ID == playlistItemId));
|
||||||
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
|
APIRoom.Playlist.RemoveAll(existing => existing.ID == playlistItemId);
|
||||||
|
|
||||||
|
Debug.Assert(Room.Playlist.Count > 0);
|
||||||
|
|
||||||
ItemRemoved?.Invoke(playlistItemId);
|
ItemRemoved?.Invoke(playlistItemId);
|
||||||
RoomUpdated?.Invoke();
|
RoomUpdated?.Invoke();
|
||||||
});
|
});
|
||||||
|
28
osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
Normal file
28
osu.Game/Online/Multiplayer/MultiplayerCountdown.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
// 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 MessagePack;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.Multiplayer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Describes the current countdown in a <see cref="MultiplayerRoom"/>.
|
||||||
|
/// </summary>
|
||||||
|
[MessagePackObject]
|
||||||
|
[Union(0, typeof(MatchStartCountdown))] // IMPORTANT: Add rules to SignalRUnionWorkaroundResolver for new derived types.
|
||||||
|
public abstract class MultiplayerCountdown
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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 TimeSpan TimeRemaining { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -54,6 +54,12 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
[Key(6)]
|
[Key(6)]
|
||||||
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
|
public IList<MultiplayerPlaylistItem> Playlist { get; set; } = new List<MultiplayerPlaylistItem>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The currently-running countdown.
|
||||||
|
/// </summary>
|
||||||
|
[Key(7)]
|
||||||
|
public MultiplayerCountdown? Countdown { get; set; }
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
[SerializationConstructor]
|
[SerializationConstructor]
|
||||||
public MultiplayerRoom(long roomId)
|
public MultiplayerRoom(long roomId)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
|
using osu.Game.Online.Multiplayer.Countdown;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
|
|
||||||
namespace osu.Game.Online
|
namespace osu.Game.Online
|
||||||
@ -18,8 +19,12 @@ namespace osu.Game.Online
|
|||||||
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
|
internal static readonly IReadOnlyList<(Type derivedType, Type baseType)> BASE_TYPE_MAPPING = new[]
|
||||||
{
|
{
|
||||||
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
|
(typeof(ChangeTeamRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(StartMatchCountdownRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(StopCountdownRequest), typeof(MatchUserRequest)),
|
||||||
|
(typeof(CountdownChangedEvent), typeof(MatchServerEvent)),
|
||||||
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
(typeof(TeamVersusRoomState), typeof(MatchRoomState)),
|
||||||
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
(typeof(TeamVersusUserState), typeof(MatchUserState)),
|
||||||
|
(typeof(MatchStartCountdown), typeof(MultiplayerCountdown))
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1046,6 +1046,10 @@ namespace osu.Game
|
|||||||
|
|
||||||
switch (e.Action)
|
switch (e.Action)
|
||||||
{
|
{
|
||||||
|
case GlobalAction.ToggleSkinEditor:
|
||||||
|
skinEditor.ToggleVisibility();
|
||||||
|
return true;
|
||||||
|
|
||||||
case GlobalAction.ResetInputSettings:
|
case GlobalAction.ResetInputSettings:
|
||||||
Host.ResetInputHandlers();
|
Host.ResetInputHandlers();
|
||||||
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
||||||
|
@ -200,7 +200,7 @@ namespace osu.Game
|
|||||||
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
|
if (Storage.Exists(DatabaseContextFactory.DATABASE_NAME))
|
||||||
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
|
dependencies.Cache(EFContextFactory = new DatabaseContextFactory(Storage));
|
||||||
|
|
||||||
dependencies.Cache(realm = new RealmAccess(Storage, "client", EFContextFactory));
|
dependencies.Cache(realm = new RealmAccess(Storage, "client", Host.UpdateThread, EFContextFactory));
|
||||||
|
|
||||||
dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
|
dependencies.CacheAs<RulesetStore>(RulesetStore = new RealmRulesetStore(realm, Storage));
|
||||||
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
dependencies.CacheAs<IRulesetStore>(RulesetStore);
|
||||||
|
@ -173,7 +173,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
{
|
{
|
||||||
Text = score.MaxCombo.ToLocalisableString(@"0\x"),
|
Text = score.MaxCombo.ToLocalisableString(@"0\x"),
|
||||||
Font = OsuFont.GetFont(size: text_size),
|
Font = OsuFont.GetFont(size: text_size),
|
||||||
|
#pragma warning disable 618
|
||||||
Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
|
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`.
|
// TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`.
|
||||||
var beatmapInfo = new BeatmapInfo
|
var beatmapInfo = new BeatmapInfo
|
||||||
{
|
{
|
||||||
|
#pragma warning disable 618
|
||||||
MaxCombo = apiBeatmap.MaxCombo,
|
MaxCombo = apiBeatmap.MaxCombo,
|
||||||
|
#pragma warning restore 618
|
||||||
Status = apiBeatmap.Status,
|
Status = apiBeatmap.Status,
|
||||||
MD5Hash = apiBeatmap.MD5Hash
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -73,6 +73,10 @@ namespace osu.Game.Overlays
|
|||||||
private Container channelSelectionContainer;
|
private Container channelSelectionContainer;
|
||||||
protected ChannelSelectionOverlay ChannelSelectionOverlay;
|
protected ChannelSelectionOverlay ChannelSelectionOverlay;
|
||||||
|
|
||||||
|
private readonly IBindableList<Channel> availableChannels = new BindableList<Channel>();
|
||||||
|
private readonly IBindableList<Channel> joinedChannels = new BindableList<Channel>();
|
||||||
|
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
|
||||||
|
|
||||||
public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos)
|
public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos)
|
||||||
|| (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos));
|
|| (ChannelSelectionOverlay.State.Value == Visibility.Visible && ChannelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos));
|
||||||
|
|
||||||
@ -198,9 +202,13 @@ namespace osu.Game.Overlays
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
availableChannels.BindTo(channelManager.AvailableChannels);
|
||||||
|
joinedChannels.BindTo(channelManager.JoinedChannels);
|
||||||
|
currentChannel.BindTo(channelManager.CurrentChannel);
|
||||||
|
|
||||||
textBox.OnCommit += postMessage;
|
textBox.OnCommit += postMessage;
|
||||||
|
|
||||||
ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
|
ChannelTabControl.Current.ValueChanged += current => currentChannel.Value = current.NewValue;
|
||||||
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
|
ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
|
||||||
ChannelSelectionOverlay.State.ValueChanged += state =>
|
ChannelSelectionOverlay.State.ValueChanged += state =>
|
||||||
{
|
{
|
||||||
@ -238,18 +246,12 @@ namespace osu.Game.Overlays
|
|||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
|
// TODO: consider scheduling bindable callbacks to not perform when overlay is not present.
|
||||||
channelManager.JoinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
joinedChannels.BindCollectionChanged(joinedChannelsChanged, true);
|
||||||
|
availableChannels.BindCollectionChanged(availableChannelsChanged, true);
|
||||||
channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged;
|
|
||||||
availableChannelsChanged(null, null);
|
|
||||||
|
|
||||||
currentChannel = channelManager.CurrentChannel.GetBoundCopy();
|
|
||||||
currentChannel.BindValueChanged(currentChannelChanged, true);
|
currentChannel.BindValueChanged(currentChannelChanged, true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bindable<Channel> currentChannel;
|
|
||||||
|
|
||||||
private void currentChannelChanged(ValueChangedEvent<Channel> e)
|
private void currentChannelChanged(ValueChangedEvent<Channel> e)
|
||||||
{
|
{
|
||||||
if (e.NewValue == null)
|
if (e.NewValue == null)
|
||||||
@ -318,7 +320,7 @@ namespace osu.Game.Overlays
|
|||||||
if (!channel.Joined.Value)
|
if (!channel.Joined.Value)
|
||||||
channel = channelManager.JoinChannel(channel);
|
channel = channelManager.JoinChannel(channel);
|
||||||
|
|
||||||
channelManager.CurrentChannel.Value = channel;
|
currentChannel.Value = channel;
|
||||||
}
|
}
|
||||||
|
|
||||||
channel.HighlightedMessage.Value = message;
|
channel.HighlightedMessage.Value = message;
|
||||||
@ -407,7 +409,7 @@ namespace osu.Game.Overlays
|
|||||||
return true;
|
return true;
|
||||||
|
|
||||||
case PlatformAction.DocumentClose:
|
case PlatformAction.DocumentClose:
|
||||||
channelManager.LeaveChannel(channelManager.CurrentChannel.Value);
|
channelManager.LeaveChannel(currentChannel.Value);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -487,19 +489,7 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args)
|
||||||
{
|
{
|
||||||
ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels);
|
ChannelSelectionOverlay.UpdateAvailableChannels(availableChannels);
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
|
||||||
{
|
|
||||||
base.Dispose(isDisposing);
|
|
||||||
|
|
||||||
if (channelManager != null)
|
|
||||||
{
|
|
||||||
channelManager.CurrentChannel.ValueChanged -= currentChannelChanged;
|
|
||||||
channelManager.JoinedChannels.CollectionChanged -= joinedChannelsChanged;
|
|
||||||
channelManager.AvailableChannels.CollectionChanged -= availableChannelsChanged;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void postMessage(TextBox textBox, bool newText)
|
private void postMessage(TextBox textBox, bool newText)
|
||||||
|
@ -219,7 +219,12 @@ namespace osu.Game.Overlays.Dialog
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Programmatically clicks the first <see cref="PopupDialogOkButton"/>.
|
/// Programmatically clicks the first <see cref="PopupDialogOkButton"/>.
|
||||||
/// </summary>
|
/// </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)
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
|||||||
Add(new SettingsButton
|
Add(new SettingsButton
|
||||||
{
|
{
|
||||||
Text = GeneralSettingsStrings.OpenOsuFolder,
|
Text = GeneralSettingsStrings.OpenOsuFolder,
|
||||||
Action = storage.PresentExternally,
|
Action = () => storage.PresentExternally(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Add(new SettingsButton
|
Add(new SettingsButton
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
|||||||
new SettingsButton
|
new SettingsButton
|
||||||
{
|
{
|
||||||
Text = SkinSettingsStrings.SkinLayoutEditor,
|
Text = SkinSettingsStrings.SkinLayoutEditor,
|
||||||
Action = () => skinEditor?.Toggle(),
|
Action = () => skinEditor?.ToggleVisibility(),
|
||||||
},
|
},
|
||||||
new ExportSkinButton(),
|
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));
|
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
|
||||||
|
|
||||||
public bool Equals(LegacyHitSampleInfo? other)
|
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)
|
public override bool Equals(object? obj)
|
||||||
=> obj is LegacyHitSampleInfo other && Equals(other);
|
=> 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>
|
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||||
|
@ -122,7 +122,19 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
public static class HitResultExtensions
|
public static class HitResultExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public static bool AffectsCombo(this HitResult result)
|
public static bool AffectsCombo(this HitResult result)
|
||||||
{
|
{
|
||||||
|
@ -166,20 +166,10 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
if (!result.Type.IsScorable())
|
if (!result.Type.IsScorable())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (result.Type.AffectsCombo())
|
if (result.Type.IncreasesCombo())
|
||||||
{
|
Combo.Value++;
|
||||||
switch (result.Type)
|
else if (result.Type.BreaksCombo())
|
||||||
{
|
Combo.Value = 0;
|
||||||
case HitResult.Miss:
|
|
||||||
case HitResult.LargeTickMiss:
|
|
||||||
Combo.Value = 0;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
Combo.Value++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0;
|
||||||
|
|
||||||
|
@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI
|
|||||||
/// </param>
|
/// </param>
|
||||||
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
|
||||||
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</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 TObject : HitObject
|
||||||
where TDrawable : DrawableHitObject, new()
|
where TDrawable : DrawableHitObject, new()
|
||||||
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
||||||
|
@ -23,6 +23,8 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
private IBeatmap currentBeatmap;
|
private IBeatmap currentBeatmap;
|
||||||
private Ruleset currentRuleset;
|
private Ruleset currentRuleset;
|
||||||
|
|
||||||
|
private float beatmapOffset;
|
||||||
|
|
||||||
public Score Parse(Stream stream)
|
public Score Parse(Stream stream)
|
||||||
{
|
{
|
||||||
var score = new Score
|
var score = new Score
|
||||||
@ -72,6 +74,9 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
|
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
|
||||||
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
|
scoreInfo.BeatmapInfo = currentBeatmap.BeatmapInfo;
|
||||||
|
|
||||||
|
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
|
||||||
|
beatmapOffset = currentBeatmap.BeatmapInfo.BeatmapVersion < 5 ? LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
|
||||||
|
|
||||||
/* score.HpGraphString = */
|
/* score.HpGraphString = */
|
||||||
sr.ReadString();
|
sr.ReadString();
|
||||||
|
|
||||||
@ -229,7 +234,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
|
|
||||||
private void readLegacyReplay(Replay replay, StreamReader reader)
|
private void readLegacyReplay(Replay replay, StreamReader reader)
|
||||||
{
|
{
|
||||||
float lastTime = 0;
|
float lastTime = beatmapOffset;
|
||||||
ReplayFrame currentFrame = null;
|
ReplayFrame currentFrame = null;
|
||||||
|
|
||||||
string[] frames = reader.ReadToEnd().Split(',');
|
string[] frames = reader.ReadToEnd().Split(',');
|
||||||
|
@ -1,12 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Beatmaps.Formats;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
using osu.Game.IO.Legacy;
|
using osu.Game.IO.Legacy;
|
||||||
using osu.Game.Replays.Legacy;
|
using osu.Game.Replays.Legacy;
|
||||||
@ -14,8 +17,6 @@ using osu.Game.Rulesets.Replays;
|
|||||||
using osu.Game.Rulesets.Replays.Types;
|
using osu.Game.Rulesets.Replays.Types;
|
||||||
using SharpCompress.Compressors.LZMA;
|
using SharpCompress.Compressors.LZMA;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace osu.Game.Scoring.Legacy
|
namespace osu.Game.Scoring.Legacy
|
||||||
{
|
{
|
||||||
public class LegacyScoreEncoder
|
public class LegacyScoreEncoder
|
||||||
@ -111,6 +112,9 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
{
|
{
|
||||||
StringBuilder replayData = new StringBuilder();
|
StringBuilder replayData = new StringBuilder();
|
||||||
|
|
||||||
|
// As this is baked into hitobject timing (see `LegacyBeatmapDecoder`) we also need to apply this to replay frame timing.
|
||||||
|
double offset = beatmap?.BeatmapInfo.BeatmapVersion < 5 ? -LegacyBeatmapDecoder.EARLY_VERSION_TIMING_OFFSET : 0;
|
||||||
|
|
||||||
if (score.Replay != null)
|
if (score.Replay != null)
|
||||||
{
|
{
|
||||||
int lastTime = 0;
|
int lastTime = 0;
|
||||||
@ -120,7 +124,7 @@ namespace osu.Game.Scoring.Legacy
|
|||||||
var legacyFrame = getLegacyFrame(f);
|
var legacyFrame = getLegacyFrame(f);
|
||||||
|
|
||||||
// Rounding because stable could only parse integral values
|
// Rounding because stable could only parse integral values
|
||||||
int time = (int)Math.Round(legacyFrame.Time);
|
int time = (int)Math.Round(legacyFrame.Time + offset);
|
||||||
replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
|
replayData.Append(FormattableString.Invariant($"{time - lastTime}|{legacyFrame.MouseX ?? 0}|{legacyFrame.MouseY ?? 0}|{(int)legacyFrame.ButtonState},"));
|
||||||
lastTime = time;
|
lastTime = time;
|
||||||
}
|
}
|
||||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Scoring
|
|||||||
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
|
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
|
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Ignored]
|
[Ignored]
|
||||||
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
|
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
|
||||||
|
@ -134,35 +134,9 @@ namespace osu.Game.Scoring
|
|||||||
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
||||||
return score.TotalScore;
|
return score.TotalScore;
|
||||||
|
|
||||||
int beatmapMaxCombo;
|
int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (beatmapMaxCombo == null)
|
||||||
if (score.IsLegacyScore)
|
return score.TotalScore;
|
||||||
{
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (beatmapMaxCombo == 0)
|
if (beatmapMaxCombo == 0)
|
||||||
return 0;
|
return 0;
|
||||||
@ -171,7 +145,37 @@ namespace osu.Game.Scoring
|
|||||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||||
scoreProcessor.Mods.Value = score.Mods;
|
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>
|
/// <summary>
|
||||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
private bool canSave;
|
private bool canSave;
|
||||||
|
|
||||||
private bool exitConfirmed;
|
protected bool ExitConfirmed { get; private set; }
|
||||||
|
|
||||||
private string lastSavedHash;
|
private string lastSavedHash;
|
||||||
|
|
||||||
@ -586,7 +586,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
public override bool OnExiting(IScreen next)
|
public override bool OnExiting(IScreen next)
|
||||||
{
|
{
|
||||||
if (!exitConfirmed)
|
if (!ExitConfirmed)
|
||||||
{
|
{
|
||||||
// dialog overlay may not be available in visual tests.
|
// dialog overlay may not be available in visual tests.
|
||||||
if (dialogOverlay == null)
|
if (dialogOverlay == null)
|
||||||
@ -595,12 +595,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if the dialog is already displayed, confirm exit with no save.
|
// if the dialog is already displayed, block exiting until the user explicitly makes a decision.
|
||||||
if (dialogOverlay.CurrentDialog is PromptForSaveDialog saveDialog)
|
if (dialogOverlay.CurrentDialog is PromptForSaveDialog)
|
||||||
{
|
|
||||||
saveDialog.PerformOkAction();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
|
|
||||||
if (isNewBeatmap || HasUnsavedChanges)
|
if (isNewBeatmap || HasUnsavedChanges)
|
||||||
{
|
{
|
||||||
@ -645,7 +642,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
Save();
|
Save();
|
||||||
|
|
||||||
exitConfirmed = true;
|
ExitConfirmed = true;
|
||||||
this.Exit();
|
this.Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -668,7 +665,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
Beatmap.SetDefault();
|
Beatmap.SetDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
exitConfirmed = true;
|
ExitConfirmed = true;
|
||||||
this.Exit();
|
this.Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,12 +17,12 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
Buttons = new PopupDialogButton[]
|
Buttons = new PopupDialogButton[]
|
||||||
{
|
{
|
||||||
new PopupDialogCancelButton
|
new PopupDialogOkButton
|
||||||
{
|
{
|
||||||
Text = @"Save my masterpiece!",
|
Text = @"Save my masterpiece!",
|
||||||
Action = saveAndExit
|
Action = saveAndExit
|
||||||
},
|
},
|
||||||
new PopupDialogOkButton
|
new PopupDialogDangerousButton
|
||||||
{
|
{
|
||||||
Text = @"Forget all changes",
|
Text = @"Forget all changes",
|
||||||
Action = exit
|
Action = exit
|
||||||
|
@ -79,10 +79,10 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
private readonly ButtonArea buttonArea;
|
private readonly ButtonArea buttonArea;
|
||||||
|
|
||||||
private readonly Button backButton;
|
private readonly MainMenuButton backButton;
|
||||||
|
|
||||||
private readonly List<Button> buttonsTopLevel = new List<Button>();
|
private readonly List<MainMenuButton> buttonsTopLevel = new List<MainMenuButton>();
|
||||||
private readonly List<Button> buttonsPlay = new List<Button>();
|
private readonly List<MainMenuButton> buttonsPlay = new List<MainMenuButton>();
|
||||||
|
|
||||||
private Sample sampleBack;
|
private Sample sampleBack;
|
||||||
|
|
||||||
@ -100,8 +100,8 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
buttonArea.AddRange(new Drawable[]
|
buttonArea.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
new Button(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
|
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, FontAwesome.Solid.Cog, new Color4(85, 85, 85, 255), () => OnSettings?.Invoke(), -WEDGE_WIDTH, Key.O),
|
||||||
backButton = new Button(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
|
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"button-back-select", OsuIcon.LeftCircle, new Color4(51, 58, 94, 255), () => State = ButtonSystemState.TopLevel, -WEDGE_WIDTH)
|
||||||
{
|
{
|
||||||
VisibleState = ButtonSystemState.Play,
|
VisibleState = ButtonSystemState.Play,
|
||||||
},
|
},
|
||||||
@ -126,24 +126,24 @@ namespace osu.Game.Screens.Menu
|
|||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
|
private void load(AudioManager audio, IdleTracker idleTracker, GameHost host)
|
||||||
{
|
{
|
||||||
buttonsPlay.Add(new Button(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P));
|
||||||
buttonsPlay.Add(new Button(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Multi, @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M));
|
||||||
buttonsPlay.Add(new Button(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
|
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Playlists, @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L));
|
||||||
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
|
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
|
||||||
|
|
||||||
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
|
||||||
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
|
||||||
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
|
||||||
|
|
||||||
if (host.CanExit)
|
if (host.CanExit)
|
||||||
buttonsTopLevel.Add(new Button(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
|
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q));
|
||||||
|
|
||||||
buttonArea.AddRange(buttonsPlay);
|
buttonArea.AddRange(buttonsPlay);
|
||||||
buttonArea.AddRange(buttonsTopLevel);
|
buttonArea.AddRange(buttonsTopLevel);
|
||||||
|
|
||||||
buttonArea.ForEach(b =>
|
buttonArea.ForEach(b =>
|
||||||
{
|
{
|
||||||
if (b is Button)
|
if (b is MainMenuButton)
|
||||||
{
|
{
|
||||||
b.Origin = Anchor.CentreLeft;
|
b.Origin = Anchor.CentreLeft;
|
||||||
b.Anchor = Anchor.CentreLeft;
|
b.Anchor = Anchor.CentreLeft;
|
||||||
@ -305,7 +305,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
{
|
{
|
||||||
buttonArea.ButtonSystemState = state;
|
buttonArea.ButtonSystemState = state;
|
||||||
|
|
||||||
foreach (var b in buttonArea.Children.OfType<Button>())
|
foreach (var b in buttonArea.Children.OfType<MainMenuButton>())
|
||||||
b.ButtonSystemState = state;
|
b.ButtonSystemState = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
/// Button designed specifically for the osu!next main menu.
|
/// Button designed specifically for the osu!next main menu.
|
||||||
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
|
/// In order to correctly flow, we have to use a negative margin on the parent container (due to the parallelogram shape).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class Button : BeatSyncedContainer, IStateful<ButtonState>
|
public class MainMenuButton : BeatSyncedContainer, IStateful<ButtonState>
|
||||||
{
|
{
|
||||||
public event Action<ButtonState> StateChanged;
|
public event Action<ButtonState> StateChanged;
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
public Button(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
|
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action clickAction = null, float extraWidth = 0, Key triggerKey = Key.Unknown)
|
||||||
{
|
{
|
||||||
this.sampleName = sampleName;
|
this.sampleName = sampleName;
|
||||||
this.clickAction = clickAction;
|
this.clickAction = clickAction;
|
||||||
@ -209,7 +209,7 @@ namespace osu.Game.Screens.Menu
|
|||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
|
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
|
if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
|
@ -15,12 +15,12 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
|||||||
{
|
{
|
||||||
public new readonly BindableBool Enabled = new BindableBool();
|
public new readonly BindableBool Enabled = new BindableBool();
|
||||||
|
|
||||||
private IBindable<BeatmapAvailability> availability;
|
private readonly IBindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
|
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
|
||||||
{
|
{
|
||||||
availability = beatmapTracker.Availability.GetBoundCopy();
|
availability.BindTo(beatmapTracker.Availability);
|
||||||
|
|
||||||
availability.BindValueChanged(_ => updateState());
|
availability.BindValueChanged(_ => updateState());
|
||||||
Enabled.BindValueChanged(_ => updateState(), true);
|
Enabled.BindValueChanged(_ => updateState(), true);
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
@ -100,122 +101,126 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
|||||||
{
|
{
|
||||||
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
|
sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection");
|
||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChild = new PopoverContainer
|
||||||
{
|
{
|
||||||
beatmapAvailabilityTracker,
|
RelativeSizeAxes = Axes.Both,
|
||||||
new MultiplayerRoomSounds(),
|
Children = new Drawable[]
|
||||||
new GridContainer
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
beatmapAvailabilityTracker,
|
||||||
RowDimensions = new[]
|
new MultiplayerRoomSounds(),
|
||||||
|
new GridContainer
|
||||||
{
|
{
|
||||||
new Dimension(),
|
RelativeSizeAxes = Axes.Both,
|
||||||
new Dimension(GridSizeMode.Absolute, 50)
|
RowDimensions = new[]
|
||||||
},
|
|
||||||
Content = new[]
|
|
||||||
{
|
|
||||||
// Padded main content (drawable room + main content)
|
|
||||||
new Drawable[]
|
|
||||||
{
|
{
|
||||||
new Container
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.Absolute, 50)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
// Padded main content (drawable room + main content)
|
||||||
|
new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
new Container
|
||||||
Padding = new MarginPadding
|
|
||||||
{
|
{
|
||||||
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Bottom = 30
|
Padding = new MarginPadding
|
||||||
},
|
|
||||||
Children = new[]
|
|
||||||
{
|
|
||||||
mainContent = new GridContainer
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
|
||||||
RowDimensions = new[]
|
Bottom = 30
|
||||||
|
},
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
mainContent = new GridContainer
|
||||||
{
|
{
|
||||||
new Dimension(GridSizeMode.AutoSize),
|
RelativeSizeAxes = Axes.Both,
|
||||||
new Dimension(GridSizeMode.Absolute, 10)
|
RowDimensions = new[]
|
||||||
},
|
|
||||||
Content = new[]
|
|
||||||
{
|
|
||||||
new Drawable[]
|
|
||||||
{
|
{
|
||||||
new DrawableMatchRoom(Room, allowEdit)
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
{
|
new Dimension(GridSizeMode.Absolute, 10)
|
||||||
OnEdit = () => settingsOverlay.Show(),
|
|
||||||
SelectedItem = { BindTarget = SelectedItem }
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
null,
|
Content = new[]
|
||||||
new Drawable[]
|
|
||||||
{
|
{
|
||||||
new Container
|
new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
new DrawableMatchRoom(Room, allowEdit)
|
||||||
Children = new[]
|
|
||||||
{
|
{
|
||||||
new Container
|
OnEdit = () => settingsOverlay.Show(),
|
||||||
|
SelectedItem = { BindTarget = SelectedItem }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
new Drawable[]
|
||||||
|
{
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
new Container
|
||||||
Masking = true,
|
|
||||||
CornerRadius = 10,
|
|
||||||
Child = new Box
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
|
Masking = true,
|
||||||
|
CornerRadius = 10,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
new Container
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding(20),
|
|
||||||
Child = CreateMainContent(),
|
|
||||||
},
|
|
||||||
new Container
|
|
||||||
{
|
|
||||||
Anchor = Anchor.BottomLeft,
|
|
||||||
Origin = Anchor.BottomLeft,
|
|
||||||
RelativeSizeAxes = Axes.X,
|
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Child = userModsSelectOverlay = new UserModSelectOverlay
|
|
||||||
{
|
{
|
||||||
SelectedMods = { BindTarget = UserMods },
|
RelativeSizeAxes = Axes.Both,
|
||||||
IsValidMod = _ => false
|
Padding = new MarginPadding(20),
|
||||||
}
|
Child = CreateMainContent(),
|
||||||
},
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.BottomLeft,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = userModsSelectOverlay = new UserModSelectOverlay
|
||||||
|
{
|
||||||
|
SelectedMods = { BindTarget = UserMods },
|
||||||
|
IsValidMod = _ => false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
// Resolves 1px masking errors between the settings overlay and the room panel.
|
||||||
|
Padding = new MarginPadding(-1),
|
||||||
|
Child = settingsOverlay = CreateRoomSettingsOverlay(Room)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new Container
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
// Resolves 1px masking errors between the settings overlay and the room panel.
|
|
||||||
Padding = new MarginPadding(-1),
|
|
||||||
Child = settingsOverlay = CreateRoomSettingsOverlay(Room)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
// Footer
|
||||||
// Footer
|
new Drawable[]
|
||||||
new Drawable[]
|
|
||||||
{
|
|
||||||
new Container
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
new Container
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new Box
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
new Box
|
||||||
Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
|
{
|
||||||
},
|
RelativeSizeAxes = Axes.Both,
|
||||||
new Container
|
Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
|
||||||
{
|
},
|
||||||
RelativeSizeAxes = Axes.Both,
|
new Container
|
||||||
Padding = new MarginPadding(5),
|
{
|
||||||
Child = CreateFooter()
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
Padding = new MarginPadding(5),
|
||||||
|
Child = CreateFooter()
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
|
||||||
@ -12,19 +11,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
private const float ready_button_width = 600;
|
private const float ready_button_width = 600;
|
||||||
private const float spectate_button_width = 200;
|
private const float spectate_button_width = 200;
|
||||||
|
|
||||||
public Action OnReadyClick
|
|
||||||
{
|
|
||||||
set => readyButton.OnReadyClick = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Action OnSpectateClick
|
|
||||||
{
|
|
||||||
set => spectateButton.OnSpectateClick = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly MultiplayerReadyButton readyButton;
|
|
||||||
private readonly MultiplayerSpectateButton spectateButton;
|
|
||||||
|
|
||||||
public MultiplayerMatchFooter()
|
public MultiplayerMatchFooter()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
@ -37,12 +23,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
new Drawable[]
|
new Drawable[]
|
||||||
{
|
{
|
||||||
null,
|
null,
|
||||||
spectateButton = new MultiplayerSpectateButton
|
new MultiplayerSpectateButton
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
readyButton = new MultiplayerReadyButton
|
new MatchStartControl
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
},
|
},
|
||||||
|
@ -3,165 +3,176 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Audio.Sample;
|
|
||||||
using osu.Framework.Bindables;
|
|
||||||
using osu.Framework.Graphics;
|
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Backgrounds;
|
using osu.Game.Graphics.Backgrounds;
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Screens.OnlinePlay.Components;
|
using osu.Game.Screens.OnlinePlay.Components;
|
||||||
using osuTK;
|
|
||||||
|
|
||||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
||||||
{
|
{
|
||||||
public class MultiplayerReadyButton : MultiplayerRoomComposite
|
public class MultiplayerReadyButton : ReadyButton
|
||||||
{
|
{
|
||||||
public Action OnReadyClick
|
public new Triangles Triangles => base.Triangles;
|
||||||
{
|
|
||||||
set => button.Action = value;
|
[Resolved]
|
||||||
}
|
private MultiplayerClient multiplayerClient { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuColour colours { get; set; }
|
private OsuColour colours { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[CanBeNull]
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
private MultiplayerRoom room => multiplayerClient.Room;
|
||||||
|
|
||||||
private IBindable<bool> operationInProgress;
|
|
||||||
|
|
||||||
private Sample sampleReady;
|
|
||||||
private Sample sampleReadyAll;
|
|
||||||
private Sample sampleUnready;
|
|
||||||
|
|
||||||
private readonly ButtonWithTrianglesExposed button;
|
|
||||||
|
|
||||||
private int countReady;
|
|
||||||
|
|
||||||
private ScheduledDelegate readySampleDelegate;
|
|
||||||
|
|
||||||
public MultiplayerReadyButton()
|
|
||||||
{
|
|
||||||
InternalChild = button = new ButtonWithTrianglesExposed
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Size = Vector2.One,
|
|
||||||
Enabled = { Value = true },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
[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()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
CurrentPlaylistItem.BindValueChanged(_ => updateState());
|
multiplayerClient.RoomUpdated += onRoomUpdated;
|
||||||
|
onRoomUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnRoomUpdated()
|
private MultiplayerCountdown countdown;
|
||||||
{
|
private DateTimeOffset countdownReceivedTime;
|
||||||
base.OnRoomUpdated();
|
private ScheduledDelegate countdownUpdateDelegate;
|
||||||
|
|
||||||
updateState();
|
private void onRoomUpdated() => Scheduler.AddOnce(() =>
|
||||||
|
{
|
||||||
|
if (countdown == null && room?.Countdown != null)
|
||||||
|
countdownReceivedTime = DateTimeOffset.Now;
|
||||||
|
|
||||||
|
countdown = room?.Countdown;
|
||||||
|
|
||||||
|
if (room?.Countdown != null)
|
||||||
|
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 1000, true);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
countdownUpdateDelegate?.Cancel();
|
||||||
|
countdownUpdateDelegate = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 countText = $"({countReady} / {countTotal} ready)";
|
||||||
|
|
||||||
|
if (countdown != null)
|
||||||
|
{
|
||||||
|
TimeSpan timeElapsed = DateTimeOffset.Now - countdownReceivedTime;
|
||||||
|
TimeSpan countdownRemaining;
|
||||||
|
|
||||||
|
if (timeElapsed > countdown.TimeRemaining)
|
||||||
|
countdownRemaining = TimeSpan.Zero;
|
||||||
|
else
|
||||||
|
countdownRemaining = countdown.TimeRemaining - timeElapsed;
|
||||||
|
|
||||||
|
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
|
||||||
|
|
||||||
|
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 updateState()
|
private void updateButtonColour()
|
||||||
{
|
{
|
||||||
var localUser = Client.LocalUser;
|
if (room == null)
|
||||||
|
{
|
||||||
|
setGreen();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0;
|
var localUser = multiplayerClient.LocalUser;
|
||||||
int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0;
|
|
||||||
|
|
||||||
switch (localUser?.State)
|
switch (localUser?.State)
|
||||||
{
|
{
|
||||||
default:
|
default:
|
||||||
button.Text = "Ready";
|
setGreen();
|
||||||
updateButtonColour(true);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MultiplayerUserState.Spectating:
|
case MultiplayerUserState.Spectating:
|
||||||
case MultiplayerUserState.Ready:
|
case MultiplayerUserState.Ready:
|
||||||
string countText = $"({newCountReady} / {newCountTotal} ready)";
|
if (room?.Host?.Equals(localUser) == true && room.Countdown == null)
|
||||||
|
setGreen();
|
||||||
if (Room?.Host?.Equals(localUser) == true)
|
|
||||||
{
|
|
||||||
button.Text = $"Start match {countText}";
|
|
||||||
updateButtonColour(true);
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
setYellow();
|
||||||
button.Text = $"Waiting for host... {countText}";
|
|
||||||
updateButtonColour(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool enableButton =
|
void setYellow()
|
||||||
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)
|
|
||||||
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
|
|
||||||
|
|
||||||
button.Enabled.Value = enableButton;
|
|
||||||
|
|
||||||
if (newCountReady == countReady)
|
|
||||||
return;
|
|
||||||
|
|
||||||
readySampleDelegate?.Cancel();
|
|
||||||
readySampleDelegate = Schedule(() =>
|
|
||||||
{
|
{
|
||||||
if (newCountReady > countReady)
|
BackgroundColour = colours.YellowDark;
|
||||||
{
|
Triangles.ColourDark = colours.YellowDark;
|
||||||
if (newCountReady == newCountTotal)
|
Triangles.ColourLight = colours.Yellow;
|
||||||
sampleReadyAll?.Play();
|
|
||||||
else
|
|
||||||
sampleReady?.Play();
|
|
||||||
}
|
|
||||||
else if (newCountReady < countReady)
|
|
||||||
{
|
|
||||||
sampleUnready?.Play();
|
|
||||||
}
|
|
||||||
|
|
||||||
countReady = newCountReady;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateButtonColour(bool green)
|
|
||||||
{
|
|
||||||
if (green)
|
|
||||||
{
|
|
||||||
button.BackgroundColour = colours.Green;
|
|
||||||
button.Triangles.ColourDark = colours.Green;
|
|
||||||
button.Triangles.ColourLight = colours.GreenLight;
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
void setGreen()
|
||||||
{
|
{
|
||||||
button.BackgroundColour = colours.YellowDark;
|
BackgroundColour = colours.Green;
|
||||||
button.Triangles.ColourDark = colours.YellowDark;
|
Triangles.ColourDark = colours.Green;
|
||||||
button.Triangles.ColourLight = colours.Yellow;
|
Triangles.ColourLight = colours.GreenLight;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class ButtonWithTrianglesExposed : ReadyButton
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
public new Triangles Triangles => base.Triangles;
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
|
if (multiplayerClient != null)
|
||||||
|
multiplayerClient.RoomUpdated -= onRoomUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override LocalisableString TooltipText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (room?.Countdown != null && multiplayerClient.IsHost && multiplayerClient.LocalUser?.State == MultiplayerUserState.Ready)
|
||||||
|
return "Cancel countdown";
|
||||||
|
|
||||||
|
return base.TooltipText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -15,11 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
{
|
{
|
||||||
public class MultiplayerSpectateButton : MultiplayerRoomComposite
|
public class MultiplayerSpectateButton : MultiplayerRoomComposite
|
||||||
{
|
{
|
||||||
public Action OnSpectateClick
|
|
||||||
{
|
|
||||||
set => button.Action = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
||||||
|
|
||||||
@ -37,9 +31,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Size = Vector2.One,
|
Size = Vector2.One,
|
||||||
Enabled = { Value = true },
|
Enabled = { Value = true },
|
||||||
|
Action = onClick
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onClick()
|
||||||
|
{
|
||||||
|
var clickOperation = ongoingOperationTracker.BeginOperation();
|
||||||
|
|
||||||
|
Client.ToggleSpectate().ContinueWith(t => endOperation());
|
||||||
|
|
||||||
|
void endOperation() => clickOperation?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
|
||||||
using osu.Game.Online.Multiplayer;
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
|
|
||||||
@ -25,6 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Action<PlaylistItem> RequestEdit;
|
public Action<PlaylistItem> RequestEdit;
|
||||||
|
|
||||||
|
private MultiplayerPlaylistTabControl playlistTabControl;
|
||||||
private MultiplayerQueueList queueList;
|
private MultiplayerQueueList queueList;
|
||||||
private MultiplayerHistoryList historyList;
|
private MultiplayerHistoryList historyList;
|
||||||
private bool firstPopulation = true;
|
private bool firstPopulation = true;
|
||||||
@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
|
|||||||
|
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
new OsuTabControl<MultiplayerPlaylistDisplayMode>
|
playlistTabControl = new MultiplayerPlaylistTabControl
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = tab_control_height,
|
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()
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,9 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -46,14 +44,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private MultiplayerClient client { get; set; }
|
private MultiplayerClient client { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private OngoingOperationTracker ongoingOperationTracker { get; set; }
|
|
||||||
|
|
||||||
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
private readonly IBindable<bool> isConnected = new Bindable<bool>();
|
||||||
|
|
||||||
[CanBeNull]
|
|
||||||
private IDisposable readyClickOperation;
|
|
||||||
|
|
||||||
private AddItemButton addItemButton;
|
private AddItemButton addItemButton;
|
||||||
|
|
||||||
public MultiplayerMatchSubScreen(Room room)
|
public MultiplayerMatchSubScreen(Room room)
|
||||||
@ -230,11 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
|
this.Push(new MultiplayerMatchSongSelect(Room, itemToEdit));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Drawable CreateFooter() => new MultiplayerMatchFooter
|
protected override Drawable CreateFooter() => new MultiplayerMatchFooter();
|
||||||
{
|
|
||||||
OnReadyClick = onReadyClick,
|
|
||||||
OnSpectateClick = onSpectateClick
|
|
||||||
};
|
|
||||||
|
|
||||||
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
|
protected override RoomSettingsOverlay CreateRoomSettingsOverlay(Room room) => new MultiplayerMatchSettingsOverlay(room);
|
||||||
|
|
||||||
@ -332,52 +320,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onReadyClick()
|
|
||||||
{
|
|
||||||
Debug.Assert(readyClickOperation == null);
|
|
||||||
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
|
||||||
|
|
||||||
if (client.IsHost && (client.LocalUser?.State == MultiplayerUserState.Ready || client.LocalUser?.State == MultiplayerUserState.Spectating))
|
|
||||||
{
|
|
||||||
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.
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
client.ToggleReady()
|
|
||||||
.ContinueWith(t => endOperation());
|
|
||||||
|
|
||||||
void endOperation()
|
|
||||||
{
|
|
||||||
readyClickOperation?.Dispose();
|
|
||||||
readyClickOperation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onSpectateClick()
|
|
||||||
{
|
|
||||||
Debug.Assert(readyClickOperation == null);
|
|
||||||
readyClickOperation = ongoingOperationTracker.BeginOperation();
|
|
||||||
|
|
||||||
client.ToggleSpectate().ContinueWith(t => endOperation());
|
|
||||||
|
|
||||||
void endOperation()
|
|
||||||
{
|
|
||||||
readyClickOperation?.Dispose();
|
|
||||||
readyClickOperation = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRoomUpdated()
|
private void onRoomUpdated()
|
||||||
{
|
{
|
||||||
// may happen if the client is kicked or otherwise removed from the room.
|
// may happen if the client is kicked or otherwise removed from the room.
|
||||||
@ -433,9 +375,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
StartPlay();
|
StartPlay();
|
||||||
|
|
||||||
readyClickOperation?.Dispose();
|
|
||||||
readyClickOperation = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Screen CreateGameplayScreen()
|
protected override Screen CreateGameplayScreen()
|
||||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
Client.RoomUpdated += invokeOnRoomUpdated;
|
Client.RoomUpdated += invokeOnRoomUpdated;
|
||||||
|
Client.LoadRequested += invokeOnRoomLoadRequested;
|
||||||
Client.UserLeft += invokeUserLeft;
|
Client.UserLeft += invokeUserLeft;
|
||||||
Client.UserKicked += invokeUserKicked;
|
Client.UserKicked += invokeUserKicked;
|
||||||
Client.UserJoined += invokeUserJoined;
|
Client.UserJoined += invokeUserJoined;
|
||||||
@ -38,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
|
private void invokeItemAdded(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemAdded(item));
|
||||||
private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
|
private void invokeItemRemoved(long item) => Schedule(() => PlaylistItemRemoved(item));
|
||||||
private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
|
private void invokeItemChanged(MultiplayerPlaylistItem item) => Schedule(() => PlaylistItemChanged(item));
|
||||||
|
private void invokeOnRoomLoadRequested() => Scheduler.AddOnce(OnRoomLoadRequested);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when a user has joined the room.
|
/// Invoked when a user has joined the room.
|
||||||
@ -94,6 +96,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Invoked when the room requests the local user to load into gameplay.
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnRoomLoadRequested()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
if (Client != null)
|
if (Client != null)
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -187,9 +186,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
const double fade_time = 50;
|
const double fade_time = 50;
|
||||||
|
|
||||||
var currentItem = Playlist.GetCurrentItem();
|
var currentItem = Playlist.GetCurrentItem();
|
||||||
Debug.Assert(currentItem != null);
|
var ruleset = currentItem != null ? rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance() : null;
|
||||||
|
|
||||||
var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
|
|
||||||
|
|
||||||
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
|
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
|
||||||
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
|
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;
|
||||||
@ -201,15 +198,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
else
|
else
|
||||||
userModsDisplay.FadeOut(fade_time);
|
userModsDisplay.FadeOut(fade_time);
|
||||||
|
|
||||||
if (Client.IsHost && !User.Equals(Client.LocalUser))
|
kickButton.Alpha = Client.IsHost && !User.Equals(Client.LocalUser) ? 1 : 0;
|
||||||
kickButton.FadeIn(fade_time);
|
crown.Alpha = Room.Host?.Equals(User) == true ? 1 : 0;
|
||||||
else
|
|
||||||
kickButton.FadeOut(fade_time);
|
|
||||||
|
|
||||||
if (Room.Host?.Equals(User) == true)
|
|
||||||
crown.FadeIn(fade_time);
|
|
||||||
else
|
|
||||||
crown.FadeOut(fade_time);
|
|
||||||
|
|
||||||
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
|
// If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187
|
||||||
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
|
// This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix.
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
{
|
{
|
||||||
private FillFlowContainer<ParticipantPanel> panels;
|
private FillFlowContainer<ParticipantPanel> panels;
|
||||||
|
|
||||||
|
[CanBeNull]
|
||||||
|
private ParticipantPanel currentHostPanel;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -55,6 +59,24 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
|
|||||||
// Add panels for all users new to the room.
|
// Add panels for all users new to the room.
|
||||||
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
|
foreach (var user in Room.Users.Except(panels.Select(p => p.User)))
|
||||||
panels.Add(new ParticipantPanel(user));
|
panels.Add(new ParticipantPanel(user));
|
||||||
|
|
||||||
|
if (currentHostPanel == null || !currentHostPanel.User.Equals(Room.Host))
|
||||||
|
{
|
||||||
|
// Reset position of previous host back to normal, if one existing.
|
||||||
|
if (currentHostPanel != null && panels.Contains(currentHostPanel))
|
||||||
|
panels.SetLayoutPosition(currentHostPanel, 0);
|
||||||
|
|
||||||
|
currentHostPanel = null;
|
||||||
|
|
||||||
|
// Change position of new host to display above all participants.
|
||||||
|
if (Room.Host != null)
|
||||||
|
{
|
||||||
|
currentHostPanel = panels.SingleOrDefault(u => u.User.Equals(Room.Host));
|
||||||
|
|
||||||
|
if (currentHostPanel != null)
|
||||||
|
panels.SetLayoutPosition(currentHostPanel, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +115,8 @@ namespace osu.Game.Screens.OnlinePlay
|
|||||||
this.FadeIn();
|
this.FadeIn();
|
||||||
waves.Show();
|
waves.Show();
|
||||||
|
|
||||||
|
Mods.SetDefault();
|
||||||
|
|
||||||
if (loungeSubScreen.IsCurrentScreen())
|
if (loungeSubScreen.IsCurrentScreen())
|
||||||
loungeSubScreen.OnEntering(last);
|
loungeSubScreen.OnEntering(last);
|
||||||
else
|
else
|
||||||
|
@ -4,12 +4,16 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
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.Judgements;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -19,11 +23,27 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
public class BarHitErrorMeter : HitErrorMeter
|
public class BarHitErrorMeter : HitErrorMeter
|
||||||
{
|
{
|
||||||
private const int judgement_line_width = 14;
|
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 arrow;
|
||||||
private SpriteIcon iconEarly;
|
private Drawable labelEarly;
|
||||||
private SpriteIcon iconLate;
|
private Drawable labelLate;
|
||||||
|
|
||||||
private Container colourBarsEarly;
|
private Container colourBarsEarly;
|
||||||
private Container colourBarsLate;
|
private Container colourBarsLate;
|
||||||
@ -32,6 +52,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
private double maxHitWindow;
|
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()
|
public BarHitErrorMeter()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Both;
|
AutoSizeAxes = Axes.Both;
|
||||||
@ -40,13 +72,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
const int centre_marker_size = 8;
|
|
||||||
const int bar_height = 200;
|
const int bar_height = 200;
|
||||||
const int bar_width = 2;
|
const int bar_width = 2;
|
||||||
const float chevron_size = 8;
|
const float chevron_size = 8;
|
||||||
const float icon_size = 14;
|
|
||||||
|
|
||||||
var hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
hitWindows = HitWindows.GetAllAvailableWindows().ToArray();
|
||||||
|
|
||||||
InternalChild = new Container
|
InternalChild = new Container
|
||||||
{
|
{
|
||||||
@ -65,22 +95,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Children = new Drawable[]
|
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
|
colourBarsEarly = new Container
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
@ -98,14 +112,6 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Height = 0.5f,
|
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
|
judgementsContainer = new Container
|
||||||
{
|
{
|
||||||
Name = "judgements",
|
Name = "judgements",
|
||||||
@ -114,24 +120,18 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
Width = judgement_line_width,
|
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",
|
Name = "average chevron",
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreRight,
|
||||||
Width = chevron_size,
|
Width = chevron_size,
|
||||||
|
X = chevron_size,
|
||||||
RelativeSizeAxes = Axes.Y,
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Alpha = 0,
|
||||||
|
Scale = new Vector2(0, 1),
|
||||||
Child = arrow = new SpriteIcon
|
Child = arrow = new SpriteIcon
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
@ -155,8 +155,180 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
colourBars.Height = 0;
|
colourBars.Height = 0;
|
||||||
colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
|
colourBars.ResizeHeightTo(1, 800, Easing.OutQuint);
|
||||||
|
|
||||||
arrow.Alpha = 0;
|
CentreMarkerStyle.BindValueChanged(style => recreateCentreMarker(style.NewValue), true);
|
||||||
arrow.Delay(200).FadeInFromZero(600);
|
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()
|
protected override void Update()
|
||||||
@ -164,8 +336,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
// undo any layout rotation to display icons in the correct orientation
|
// undo any layout rotation to display icons in the correct orientation
|
||||||
iconEarly.Rotation = -Rotation;
|
if (labelEarly != null) labelEarly.Rotation = -Rotation;
|
||||||
iconLate.Rotation = -Rotation;
|
if (labelLate != null) labelLate.Rotation = -Rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createColourBars((HitResult result, double length)[] windows)
|
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)
|
protected override void OnNewJudgement(JudgementResult judgement)
|
||||||
{
|
{
|
||||||
const int arrow_move_duration = 800;
|
const int arrow_move_duration = 800;
|
||||||
@ -255,6 +422,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
judgementsContainer.Add(new JudgementLine
|
judgementsContainer.Add(new JudgementLine
|
||||||
{
|
{
|
||||||
|
JudgementLineThickness = { BindTarget = JudgementLineThickness },
|
||||||
Y = getRelativeJudgementPosition(judgement.TimeOffset),
|
Y = getRelativeJudgementPosition(judgement.TimeOffset),
|
||||||
Colour = GetColourForHitResult(judgement.Type),
|
Colour = GetColourForHitResult(judgement.Type),
|
||||||
});
|
});
|
||||||
@ -268,11 +436,12 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
|
|
||||||
internal class JudgementLine : CompositeDrawable
|
internal class JudgementLine : CompositeDrawable
|
||||||
{
|
{
|
||||||
|
public readonly BindableNumber<float> JudgementLineThickness = new BindableFloat();
|
||||||
|
|
||||||
public JudgementLine()
|
public JudgementLine()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
RelativePositionAxes = Axes.Y;
|
RelativePositionAxes = Axes.Y;
|
||||||
Height = judgement_line_height;
|
|
||||||
|
|
||||||
Blending = BlendingParameters.Additive;
|
Blending = BlendingParameters.Additive;
|
||||||
|
|
||||||
@ -295,6 +464,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
|
|||||||
Alpha = 0;
|
Alpha = 0;
|
||||||
Width = 0;
|
Width = 0;
|
||||||
|
|
||||||
|
JudgementLineThickness.BindValueChanged(thickness => Height = thickness.NewValue, true);
|
||||||
|
|
||||||
this
|
this
|
||||||
.FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint)
|
.FadeTo(0.6f, judgement_fade_in_duration, Easing.OutQuint)
|
||||||
.ResizeWidthTo(1, 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 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)
|
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);
|
return base.OnMouseMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user