Compare commits
55 Commits
37b9f91d42
...
3bd996ee43
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bd996ee43 | |||
|
|
80474565fc | ||
|
|
89f2c7160d | ||
|
|
f0ca079fe6 | ||
|
|
19b6761697 | ||
|
|
edf08b176a | ||
|
|
7b952b83bf | ||
|
|
214122f633 | ||
|
|
76c0bd4750 | ||
|
|
4bf3d9397f | ||
|
|
8b778e8106 | ||
|
|
ce5e54c9d2 | ||
|
|
45e8df7af2 | ||
|
|
1c30cb8371 | ||
|
|
bd4ed49c06 | ||
|
|
a593a40429 | ||
|
|
02b88de76e | ||
|
|
b64abbf1f5 | ||
|
|
4265e72180 | ||
|
|
cb9d9734d6 | ||
|
|
5763b7dbe9 | ||
|
|
e1baa03622 | ||
|
|
4f783f8c41 | ||
|
|
4c72a60ee2 | ||
|
|
013de9f85d | ||
|
|
cd6c9405fe | ||
|
|
822cb9e2fb | ||
|
|
680614fbee | ||
|
|
cb8ddc706f | ||
|
|
04d2ce150a | ||
|
|
eaffb89b4c | ||
|
|
650a61539b | ||
|
|
75bc934aa5 | ||
|
|
8d80e2bd2c | ||
|
|
34a3b1ba78 | ||
|
|
b354fa4472 | ||
|
|
1fbe1bd6c9 | ||
|
|
3c215f6574 | ||
|
|
8c28d26130 | ||
|
|
933fbd274d | ||
|
|
55ae7e8bb8 | ||
|
|
4a22ef88ce | ||
|
|
43ca046f9b | ||
|
|
dbefba57ce | ||
|
|
20904de276 | ||
|
|
fb2fe65a77 | ||
|
|
4662c5d678 | ||
|
|
d98cb9ca45 | ||
|
|
a7e4aa8b12 | ||
|
|
78f639d760 | ||
|
|
4ea03d0e07 | ||
|
|
cf0e5edf34 | ||
|
|
a825104688 | ||
|
|
fadcb9882c | ||
|
|
0558f9f2d9 |
@@ -73,6 +73,9 @@ Aside from the above, below is a brief checklist of things to watch out when you
|
||||
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
|
||||
|
||||
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
|
||||
- Please pick the following target branch for your pull request:
|
||||
- `pp-dev`, if the change impacts star rating or performance points calculations for any of the rulesets,
|
||||
- `master`, otherwise.
|
||||
- Please avoid pushing untested or incomplete code.
|
||||
- Please do not force-push or rebase unless we ask you to.
|
||||
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1028.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1118.1" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
[TestCase("mania-samples")]
|
||||
[TestCase("mania-slider")] // e.g. second and fourth notes of https://osu.ppy.sh/beatmapsets/73883#mania/216407
|
||||
[TestCase("slider-convert-samples")]
|
||||
[TestCase("spinner-convert-samples")]
|
||||
public void Test(string name) => base.Test(name);
|
||||
|
||||
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[TimingPoints]
|
||||
0,300,4,0,2,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
444,320,1000,5,2,0:0:0:0:
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"Mappings": [{
|
||||
"StartTime": 1000.0,
|
||||
"Objects": [{
|
||||
"StartTime": 1000.0,
|
||||
"EndTime": 8000.0,
|
||||
"Column": 0,
|
||||
"PlaySlidingSamples": false,
|
||||
"NodeSamples": [
|
||||
["Gameplay/soft-hitnormal"],
|
||||
["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"]
|
||||
],
|
||||
"Samples": ["Gameplay/soft-hitnormal", "Gameplay/soft-hitfinish"],
|
||||
}]
|
||||
}]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
Mode: 0
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:5
|
||||
CircleSize:5
|
||||
OverallDifficulty:5
|
||||
ApproachRate:5
|
||||
SliderMultiplier:1.4
|
||||
SliderTickRate:1
|
||||
|
||||
[TimingPoints]
|
||||
0,500,4,2,0,100,1,0
|
||||
|
||||
[HitObjects]
|
||||
256,192,1000,8,4,8000,0:2:0:0:
|
||||
@@ -45,5 +45,19 @@ namespace osu.Game.Rulesets.Mania.Tests
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
AssertNoLookup(unwanted_sample);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConvertHitObjectCustomSampleBank()
|
||||
{
|
||||
const string beatmap_sample = "normal-hitwhistle2";
|
||||
const string user_skin_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(beatmap_sample, user_skin_sample);
|
||||
|
||||
CreateTestWithBeatmap("convert-beatmap-custom-sample-bank.osu");
|
||||
|
||||
AssertBeatmapLookup(beatmap_sample);
|
||||
AssertUserLookup(user_skin_sample);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
|
||||
Duration = endTime - HitObject.StartTime,
|
||||
Column = column,
|
||||
Samples = HitObject.Samples,
|
||||
NodeSamples = (HitObject as IHasRepeats)?.NodeSamples
|
||||
NodeSamples =
|
||||
[
|
||||
HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_NORMAL).ToList(),
|
||||
HitObject.Samples
|
||||
]
|
||||
};
|
||||
}
|
||||
else
|
||||
|
||||
@@ -64,11 +64,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
private readonly Lazy<bool> hasKeyTexture;
|
||||
|
||||
private readonly ManiaBeatmap beatmap;
|
||||
private readonly bool isBeatmapConverted;
|
||||
|
||||
public ManiaLegacySkinTransformer(ISkin skin, IBeatmap beatmap)
|
||||
: base(skin)
|
||||
{
|
||||
this.beatmap = (ManiaBeatmap)beatmap;
|
||||
isBeatmapConverted = !beatmap.BeatmapInfo.Ruleset.Equals(new ManiaRuleset().RulesetInfo);
|
||||
|
||||
isLegacySkin = new Lazy<bool>(() => GetConfig<SkinConfiguration.LegacySetting, decimal>(SkinConfiguration.LegacySetting.Version) != null);
|
||||
hasKeyTexture = new Lazy<bool>(() =>
|
||||
@@ -196,8 +198,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
|
||||
public override ISample GetSample(ISampleInfo sampleInfo)
|
||||
{
|
||||
// layered hit sounds never play in mania
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
|
||||
// layered hit sounds never play in mania-native beatmaps (but do play on converts)
|
||||
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered && !isBeatmapConverted)
|
||||
return new SampleVirtual();
|
||||
|
||||
return base.GetSample(sampleInfo);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -10,6 +11,7 @@ using osu.Framework.Input.States;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests
|
||||
@@ -58,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
|
||||
foreach (var smokeContainer in smokeContainers)
|
||||
{
|
||||
if (smokeContainer.Children.Count != 0)
|
||||
if (smokeContainer.Children.OfType<SkinnableDrawable>().Any())
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,9 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
if (LastAcceptedAction != null && nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
|
||||
LastAcceptedAction = null;
|
||||
|
||||
if (LastAcceptedAction != null && gameplayClock.IsRewinding)
|
||||
LastAcceptedAction = null;
|
||||
}
|
||||
|
||||
protected abstract bool CheckValidNewAction(OsuAction action);
|
||||
|
||||
@@ -77,9 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
||||
base.LoadComplete();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
LifetimeStart = smokeStartTime = Time.Current;
|
||||
|
||||
public void StartDrawing(double time)
|
||||
{
|
||||
LifetimeStart = smokeStartTime = time;
|
||||
LifetimeEnd = smokeEndTime = double.MaxValue;
|
||||
SmokePoints.Clear();
|
||||
lastPosition = null;
|
||||
totalDistance = pointInterval;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
@@ -19,17 +19,24 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
/// </summary>
|
||||
public partial class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
|
||||
{
|
||||
private DrawablePool<SmokeSkinnableDrawable> segmentPool = null!;
|
||||
private SmokeSkinnableDrawable? currentSegmentSkinnable;
|
||||
|
||||
private Vector2 lastMousePosition;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 _) => true;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(segmentPool = new DrawablePool<SmokeSkinnableDrawable>(10));
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
{
|
||||
if (e.Action == OsuAction.Smoke)
|
||||
{
|
||||
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
|
||||
AddInternal(currentSegmentSkinnable = segmentPool.Get(segment => segment.Segment?.StartDrawing(Time.Current)));
|
||||
|
||||
// Add initial position immediately.
|
||||
addPosition();
|
||||
@@ -59,17 +66,19 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
return base.OnMouseMove(e);
|
||||
}
|
||||
|
||||
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
|
||||
private void addPosition() => currentSegmentSkinnable?.Segment?.AddPosition(lastMousePosition, Time.Current);
|
||||
|
||||
private partial class SmokeSkinnableDrawable : SkinnableDrawable
|
||||
{
|
||||
public SmokeSegment? Segment => Drawable as SmokeSegment;
|
||||
|
||||
public override bool RemoveWhenNotAlive => true;
|
||||
|
||||
public override double LifetimeStart => Drawable.LifetimeStart;
|
||||
public override double LifetimeEnd => Drawable.LifetimeEnd;
|
||||
|
||||
public SmokeSkinnableDrawable(ISkinComponentLookup lookup, Func<ISkinComponentLookup, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
|
||||
: base(lookup, defaultImplementation, confineMode)
|
||||
public SmokeSkinnableDrawable()
|
||||
: base(new OsuSkinComponentLookup(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private int rollingHits;
|
||||
|
||||
private readonly Container tickContainer;
|
||||
private SkinnableDrawable headPiece;
|
||||
|
||||
private Color4 colourIdle;
|
||||
private Color4 colourEngaged;
|
||||
@@ -59,7 +60,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
Content.Add(tickContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MinValue
|
||||
Depth = -1,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -79,7 +80,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
protected override void RecreatePieces()
|
||||
{
|
||||
if (headPiece != null)
|
||||
Content.Remove(headPiece, true);
|
||||
|
||||
base.RecreatePieces();
|
||||
|
||||
Content.Add(headPiece = createHeadPiece());
|
||||
|
||||
updateColour();
|
||||
Height = HitObject.IsStrong ? TaikoStrongableHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE;
|
||||
}
|
||||
@@ -122,6 +129,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollBody),
|
||||
_ => new ElongatedCirclePiece());
|
||||
|
||||
private SkinnableDrawable createHeadPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollHead), _ => Empty())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Depth = -2,
|
||||
};
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
|
||||
private void onNewResult(DrawableHitObject obj, JudgementResult result)
|
||||
@@ -174,7 +187,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
private void updateColour(double fadeDuration = 0)
|
||||
{
|
||||
Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1);
|
||||
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
|
||||
|
||||
if (fadeDuration == 0)
|
||||
{
|
||||
// fade duration is 0 when calling via `RecreatePieces()`.
|
||||
// in this case we want to apply the colour *without* using transforms.
|
||||
// using transforms may result in the application of colour being undone via `DrawableHitObject.UpdateState()` clearing transforms.
|
||||
if (MainPiece.Drawable is IHasAccentColour mainPieceWithAccentColour)
|
||||
mainPieceWithAccentColour.AccentColour = newColour;
|
||||
|
||||
if (headPiece.Drawable is IHasAccentColour headPieceWithAccentColour)
|
||||
headPieceWithAccentColour.AccentColour = newColour;
|
||||
}
|
||||
else
|
||||
{
|
||||
(MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
|
||||
(headPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration);
|
||||
}
|
||||
}
|
||||
|
||||
public partial class StrongNestedHit : DrawableStrongNestedHit
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -21,14 +20,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
get
|
||||
{
|
||||
// the reason why this calculation is so involved is that the head & tail sprites have different sizes/radii.
|
||||
// therefore naively taking the SSDQs of them and making a quad out of them results in a trapezoid shape and not a box.
|
||||
var headCentre = headCircle.ScreenSpaceDrawQuad.Centre;
|
||||
var headCentre = (body.ScreenSpaceDrawQuad.TopLeft + body.ScreenSpaceDrawQuad.BottomLeft) / 2;
|
||||
var tailCentre = (tailCircle.ScreenSpaceDrawQuad.TopLeft + tailCircle.ScreenSpaceDrawQuad.BottomLeft) / 2;
|
||||
|
||||
float headRadius = headCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||
float tailRadius = tailCircle.ScreenSpaceDrawQuad.Height / 2;
|
||||
float radius = Math.Max(headRadius, tailRadius);
|
||||
float radius = body.ScreenSpaceDrawQuad.Height / 2;
|
||||
|
||||
var rectangle = new RectangleF(headCentre.X, headCentre.Y, tailCentre.X - headCentre.X, 0).Inflate(radius);
|
||||
return new Quad(rectangle.TopLeft, rectangle.TopRight, rectangle.BottomLeft, rectangle.BottomRight);
|
||||
@@ -37,8 +32,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => ScreenSpaceDrawQuad.Contains(screenSpacePos);
|
||||
|
||||
private LegacyCirclePiece headCircle = null!;
|
||||
|
||||
private Sprite body = null!;
|
||||
|
||||
private Sprite tailCircle = null!;
|
||||
@@ -66,10 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge),
|
||||
},
|
||||
headCircle = new LegacyCirclePiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
};
|
||||
|
||||
AccentColour = colours.YellowDark;
|
||||
@@ -101,7 +90,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour);
|
||||
|
||||
headCircle.AccentColour = colour;
|
||||
body.Colour = colour;
|
||||
tailCircle.Colour = colour;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
|
||||
{
|
||||
switch (taikoComponent.Component)
|
||||
{
|
||||
case TaikoSkinComponents.DrumRollHead:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyCirclePiece();
|
||||
|
||||
return null;
|
||||
|
||||
case TaikoSkinComponents.DrumRollBody:
|
||||
if (GetTexture("taiko-roll-middle") != null)
|
||||
return new LegacyDrumRoll();
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
InputDrum,
|
||||
CentreHit,
|
||||
RimHit,
|
||||
DrumRollHead,
|
||||
DrumRollBody,
|
||||
DrumRollTick,
|
||||
Swell,
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace osu.Game.Tests.Chat
|
||||
sentMessages = new List<Message>();
|
||||
silencedUserIds = new List<int>();
|
||||
|
||||
((DummyAPIAccess)API).LocalUserState.Blocks.Clear();
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
@@ -63,6 +64,10 @@ namespace osu.Game.Tests.Chat
|
||||
silencedUserIds.Clear();
|
||||
return true;
|
||||
|
||||
case GetMessagesRequest getMessages:
|
||||
getMessages.TriggerSuccess(sentMessages);
|
||||
return true;
|
||||
|
||||
case GetUpdatesRequest updatesRequest:
|
||||
updatesRequest.TriggerSuccess(new GetUpdatesResponse
|
||||
{
|
||||
@@ -161,6 +166,60 @@ namespace osu.Game.Tests.Chat
|
||||
AddUntilStep("/help command received", () => channel.Messages.Last().Content.Contains("Supported commands"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockedUserMessagesAreDeletedFromInitialMessageBatch()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
|
||||
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = "i am blocked",
|
||||
SenderId = 1234
|
||||
}));
|
||||
AddStep("mark user as blocked", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
|
||||
{
|
||||
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
|
||||
TargetID = 1234,
|
||||
}));
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel);
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBlockedUserMessagesAreDeletedImmediatelyOnBlock()
|
||||
{
|
||||
Channel channel = null;
|
||||
|
||||
AddStep("create channel", () => channel = createChannel(1, ChannelType.Public));
|
||||
|
||||
AddStep("join channel and select it", () =>
|
||||
{
|
||||
channelManager.JoinChannel(channel);
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
});
|
||||
AddStep("post a message from blocked user", () => sentMessages.Add(new Message
|
||||
{
|
||||
ChannelId = channel.Id,
|
||||
Content = "i am blocked",
|
||||
SenderId = 1234
|
||||
}));
|
||||
AddUntilStep("channel has message", () => channel.Messages, () => Is.Not.Empty);
|
||||
|
||||
AddStep("block user", () => ((DummyAPIAccess)API).LocalUserState.Blocks.Add(new APIRelation
|
||||
{
|
||||
TargetUser = new APIUser { Username = "blocked", Id = 1234 },
|
||||
TargetID = 1234,
|
||||
}));
|
||||
AddAssert("channel has no messages", () => channel.Messages, () => Is.Empty);
|
||||
}
|
||||
|
||||
private void handlePostMessageRequest(PostMessageRequest request)
|
||||
{
|
||||
var message = new Message(++currentMessageId)
|
||||
|
||||
@@ -126,6 +126,22 @@ namespace osu.Game.Tests.Gameplay
|
||||
AssertBeatmapLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a hitobject which specifies a specific sample file which doesn't exist (or isn't allowed to be looked up)
|
||||
/// falls back to a normal sample.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestFileSampleFallsBackToNormal()
|
||||
{
|
||||
const string expected_sample = "normal-hitnormal";
|
||||
|
||||
SetupSkins(null, expected_sample);
|
||||
|
||||
CreateTestWithBeatmap("file-beatmap-sample.osu");
|
||||
|
||||
AssertUserLookup(expected_sample);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that a default hitobject and control point causes <see cref="TestDefaultSampleFromUserSkin"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -220,10 +220,13 @@ namespace osu.Game.Tests.Visual.Components
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||
{
|
||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
if (registerAsOwner)
|
||||
dependencies.CacheAs<IPreviewTrackOwner>(this);
|
||||
return dependencies;
|
||||
{
|
||||
// Automatically handled by interface caching.
|
||||
return base.CreateChildDependencies(parent);
|
||||
}
|
||||
|
||||
return new DependencyContainer();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,17 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK;
|
||||
@@ -21,7 +24,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
{
|
||||
public partial class TestSceneBeatmapSelectGrid : OnlinePlayTestScene
|
||||
{
|
||||
private MultiplayerPlaylistItem[] items = null!;
|
||||
private MatchmakingPlaylistItem[] items = null!;
|
||||
|
||||
private BeatmapSelectGrid grid = null!;
|
||||
|
||||
@@ -36,24 +39,44 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
.Take(50)
|
||||
.ToArray();
|
||||
|
||||
IEnumerable<MatchmakingPlaylistItem> playlistItems;
|
||||
|
||||
if (beatmaps.Length > 0)
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
playlistItems = Enumerable.Range(1, 50).Select(i =>
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = beatmaps[i % beatmaps.Length].OnlineID,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
var beatmap = beatmaps[i % beatmaps.Length];
|
||||
|
||||
return new MatchmakingPlaylistItem(
|
||||
new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = beatmap.OnlineID,
|
||||
StarRating = i / 10.0,
|
||||
},
|
||||
CreateAPIBeatmap(beatmap),
|
||||
Array.Empty<Mod>()
|
||||
);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
items = Enumerable.Range(1, 50).Select(i => new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
}).ToArray();
|
||||
playlistItems = Enumerable.Range(1, 50).Select(i => new MatchmakingPlaylistItem(
|
||||
new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = i,
|
||||
StarRating = i / 10.0,
|
||||
},
|
||||
CreateAPIBeatmap(),
|
||||
Array.Empty<Mod>()
|
||||
));
|
||||
}
|
||||
|
||||
foreach (var item in playlistItems)
|
||||
item.Beatmap.StarRating = item.PlaylistItem.StarRating;
|
||||
|
||||
items = playlistItems.ToArray();
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
@@ -70,8 +93,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
|
||||
AddStep("add items", () =>
|
||||
{
|
||||
foreach (var item in items)
|
||||
grid.AddItem(item);
|
||||
grid.AddItems(items);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for panels", 3);
|
||||
@@ -85,17 +107,17 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
// test scene is weird.
|
||||
});
|
||||
|
||||
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
|
||||
AddStep("add selection 1", () => grid.ChildrenOfType<MatchmakingSelectPanel>().First().AddUser(new APIUser
|
||||
{
|
||||
Id = DummyAPIAccess.DUMMY_USER_ID,
|
||||
Username = "Maarvin",
|
||||
}));
|
||||
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
|
||||
AddStep("add selection 2", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(5).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
}));
|
||||
AddStep("add selection 3", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(10).First().AddUser(new APIUser
|
||||
AddStep("add selection 3", () => grid.ChildrenOfType<MatchmakingSelectPanel>().Skip(10).First().AddUser(new APIUser
|
||||
{
|
||||
Id = 1040328,
|
||||
Username = "smoogipoo",
|
||||
@@ -180,7 +202,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
|
||||
AddStep("display roll order", () =>
|
||||
{
|
||||
var panels = grid.ChildrenOfType<BeatmapSelectPanel>().ToArray();
|
||||
var panels = grid.ChildrenOfType<MatchmakingSelectPanel>().ToArray();
|
||||
|
||||
for (int i = 0; i < panels.Length; i++)
|
||||
{
|
||||
@@ -197,6 +219,23 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPresentRandomItem()
|
||||
{
|
||||
AddStep("present random item panel", () =>
|
||||
{
|
||||
grid.TransferCandidatePanelsToRollContainer(pickRandomItems(4).candidateItems.Append(-1).ToArray(), duration: 0);
|
||||
grid.ArrangeItemsForRollAnimation(duration: 0, stagger: 0);
|
||||
grid.PlayRollAnimation(-1, duration: 0);
|
||||
|
||||
Scheduler.AddDelayed(() => grid.PresentUnanimouslyChosenBeatmap(-1), 500);
|
||||
});
|
||||
|
||||
AddWaitStep("wait for animation", 5);
|
||||
|
||||
AddStep("reveal beatmap", () => grid.RevealRandomItem(items[RNG.Next(items.Length)].PlaylistItem));
|
||||
}
|
||||
|
||||
private (long[] candidateItems, long finalItem) pickRandomItems(int count)
|
||||
{
|
||||
long[] candidateItems = items.Select(it => it.ID).ToArray();
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
// 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.Graphics;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
@@ -21,17 +21,35 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () =>
|
||||
{
|
||||
var room = CreateDefaultRoom(MatchType.Matchmaking);
|
||||
room.Playlist = Enumerable.Range(1, 50).Select(i => new PlaylistItem(new MultiplayerPlaylistItem
|
||||
{
|
||||
ID = i,
|
||||
BeatmapID = 0,
|
||||
StarRating = i / 10.0,
|
||||
})).ToArray();
|
||||
|
||||
JoinRoom(room);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapPanel()
|
||||
{
|
||||
BeatmapSelectPanel? panel = null;
|
||||
MatchmakingSelectPanel? panel = null;
|
||||
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = panel = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
|
||||
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), []))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -58,47 +76,55 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
AddStep("remove peppy", () => panel!.RemoveUser(new APIUser { Id = 2 }));
|
||||
AddStep("remove maarvin", () => panel!.RemoveUser(new APIUser { Id = 6411631 }));
|
||||
|
||||
AddToggleStep("allow selection", value =>
|
||||
{
|
||||
if (panel != null)
|
||||
panel.AllowSelection = value;
|
||||
});
|
||||
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFailedBeatmapLookup()
|
||||
public void TestRandomPanel()
|
||||
{
|
||||
AddStep("setup request handle", () =>
|
||||
{
|
||||
var api = (DummyAPIAccess)API;
|
||||
var handler = api.HandleRequest;
|
||||
api.HandleRequest = req =>
|
||||
{
|
||||
switch (req)
|
||||
{
|
||||
case GetBeatmapRequest:
|
||||
case GetBeatmapsRequest:
|
||||
req.TriggerFailure(new InvalidOperationException());
|
||||
return false;
|
||||
|
||||
default:
|
||||
return handler?.Invoke(req) ?? false;
|
||||
}
|
||||
};
|
||||
});
|
||||
MatchmakingSelectPanelRandom? panel = null;
|
||||
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new BeatmapSelectPanel(new MultiplayerPlaylistItem())
|
||||
Child = panel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddToggleStep("allow selection", value => panel!.AllowSelection = value);
|
||||
|
||||
AddStep("reveal beatmap", () => panel!.RevealBeatmap(CreateAPIBeatmap(), []));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatmapWithMods()
|
||||
{
|
||||
AddStep("add panel", () =>
|
||||
{
|
||||
MatchmakingSelectPanel? panel;
|
||||
|
||||
Child = new OsuContextMenuContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = panel = new MatchmakingSelectPanelBeatmap(new MatchmakingPlaylistItem(new MultiplayerPlaylistItem(), CreateAPIBeatmap(), [new OsuModHardRock(), new OsuModDoubleTime()]))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
|
||||
panel.AddUser(new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = "peppy",
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Matchmaking.Events;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
@@ -158,5 +160,64 @@ namespace osu.Game.Tests.Visual.Matchmaking
|
||||
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InteractionSpam()
|
||||
{
|
||||
AddStep("join users", () =>
|
||||
{
|
||||
for (int i = 0; i < 7; i++)
|
||||
{
|
||||
MultiplayerClient.AddUser(new MultiplayerRoomUser(i)
|
||||
{
|
||||
User = new APIUser
|
||||
{
|
||||
Username = $"User {i}"
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
AddStep("change to grid mode", () => list.DisplayStyle = PanelDisplayStyle.Grid);
|
||||
AddStep("player jump", () => { MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely(); });
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
|
||||
AddStep("change to split mode", () => list.DisplayStyle = PanelDisplayStyle.Split);
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
|
||||
AddStep("change to hidden mode", () => list.DisplayStyle = PanelDisplayStyle.Hidden);
|
||||
AddStep("local jumping", () => jumpSpam(false));
|
||||
AddWaitStep("wait", 25);
|
||||
AddStep("group jumping spam", () => jumpSpam(true));
|
||||
AddWaitStep("wait", 25);
|
||||
}
|
||||
|
||||
private void jumpSpam(bool everyone)
|
||||
{
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
MultiplayerClient.SendUserMatchRequest(1001, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
|
||||
}, i * 150 + RNG.NextDouble(0, 140));
|
||||
|
||||
if (!everyone)
|
||||
continue;
|
||||
|
||||
for (int ii = 0; ii < 7; ii++)
|
||||
{
|
||||
int iii = ii;
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
MultiplayerClient.SendUserMatchRequest(iii, new MatchmakingAvatarActionRequest { Action = MatchmakingAvatarAction.Jump }).WaitSafely();
|
||||
}, i * 150 + RNG.NextDouble(0, 140));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Direction = FillDirection.Full,
|
||||
Padding = new MarginPadding(20),
|
||||
Spacing = new Vector2(40),
|
||||
ChildrenEnumerable = new int?[] { 64, 423, 1453, 3468, 18_367, 48_342, 178_432, 375_231, 897_783, null }.Select(createDisplay)
|
||||
ChildrenEnumerable = new int?[] { 64, 423, 1_453, 3_468, 8_367, 48_342, 78_432, 375_231, 897_783, null }.Select(createDisplay)
|
||||
};
|
||||
|
||||
private GlobalRankDisplay createDisplay(int? rank) => new GlobalRankDisplay
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Audio
|
||||
{
|
||||
/// <summary>
|
||||
@@ -10,6 +12,7 @@ namespace osu.Game.Audio
|
||||
/// <see cref="IPreviewTrackOwner"/>s can cancel the currently playing <see cref="PreviewTrack"/> through the
|
||||
/// global <see cref="PreviewTrackManager"/> if they're the owner of the playing <see cref="PreviewTrack"/>.
|
||||
/// </remarks>
|
||||
[Cached]
|
||||
public interface IPreviewTrackOwner
|
||||
{
|
||||
}
|
||||
|
||||
@@ -157,6 +157,12 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public bool Equals(IBeatmapInfo? other) => other is BeatmapInfo b && Equals(b);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return ID.GetHashCode();
|
||||
}
|
||||
|
||||
public bool AudioEquals(BeatmapInfo? other) => other != null
|
||||
&& BeatmapSet != null
|
||||
&& other.BeatmapSet != null
|
||||
|
||||
@@ -544,7 +544,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
if (!banksOnly)
|
||||
{
|
||||
int customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name)));
|
||||
string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty;
|
||||
string sampleFilename = samples.FirstOrDefault(s => s is ConvertHitObjectParser.FileHitSampleInfo)?.LookupNames.First() ?? string.Empty;
|
||||
int volume = samples.FirstOrDefault()?.Volume ?? 100;
|
||||
|
||||
// We want to ignore custom sample banks and volume when not encoding to the mania game mode,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -263,11 +264,11 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
Task.Run(() => collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -10,7 +11,7 @@ namespace osu.Game.Collections
|
||||
public class CollectionToggleMenuItem : ToggleMenuItem
|
||||
{
|
||||
public CollectionToggleMenuItem(Live<BeatmapCollection> collection, IBeatmapInfo beatmap)
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state =>
|
||||
: base(collection.PerformRead(c => c.Name), MenuItemType.Standard, state => Task.Run(() =>
|
||||
{
|
||||
collection.PerformWrite(c =>
|
||||
{
|
||||
@@ -19,7 +20,7 @@ namespace osu.Game.Collections
|
||||
else
|
||||
c.BeatmapMD5Hashes.Remove(beatmap.MD5Hash);
|
||||
});
|
||||
})
|
||||
}))
|
||||
{
|
||||
State.Value = collection.PerformRead(c => c.BeatmapMD5Hashes.Contains(beatmap.MD5Hash));
|
||||
}
|
||||
|
||||
@@ -785,32 +785,20 @@ namespace osu.Game.Graphics.Carousel
|
||||
// We are performing two important operations here:
|
||||
// - Update all Y positions. After a selection occurs, panels may have changed visibility state and therefore Y positions.
|
||||
// - Link selected models to CarouselItems. If a selection changed, this is where we find the relevant CarouselItems for further use.
|
||||
FindCarouselItemsForSelection(ref currentKeyboardSelection, ref currentSelection, carouselItems);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var item = carouselItems[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, currentKeyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, currentSelection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = new Selection(currentKeyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = new Selection(currentSelection.Model, item, null, i);
|
||||
|
||||
updateItemYPosition(item, ref lastVisible, ref yPos);
|
||||
|
||||
if (isKeyboardSelection)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
|
||||
if (isSelection)
|
||||
currentSelection = currentSelection with { YPosition = item.CarouselYPosition + item.DrawHeight / 2 };
|
||||
}
|
||||
|
||||
if (currentKeyboardSelection.CarouselItem is CarouselItem currentKeyboardSelectionItem)
|
||||
currentKeyboardSelection = currentKeyboardSelection with { YPosition = currentKeyboardSelectionItem.CarouselYPosition + currentKeyboardSelectionItem.DrawHeight / 2 };
|
||||
|
||||
if (currentSelection.CarouselItem is CarouselItem currentSelectionItem)
|
||||
currentSelection = currentSelection with { YPosition = currentSelectionItem.CarouselYPosition + currentSelectionItem.DrawHeight / 2 };
|
||||
|
||||
// Update the total height of all items (to make the scroll container scrollable through the full height even though
|
||||
// most items are not displayed / loaded).
|
||||
Scroll.SetLayoutHeight(yPos + visibleHalfHeight);
|
||||
@@ -821,6 +809,27 @@ namespace osu.Game.Graphics.Carousel
|
||||
Scroll.OffsetScrollPosition((float)(currentKeyboardSelection.YPosition!.Value - prevKeyboard.YPosition.Value));
|
||||
}
|
||||
|
||||
protected virtual void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
|
||||
{
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
var item = items[i];
|
||||
|
||||
bool isKeyboardSelection = CheckModelEquality(item.Model, keyboardSelection.Model!);
|
||||
bool isSelection = CheckModelEquality(item.Model, selection.Model!);
|
||||
|
||||
// while we don't know the Y position of the item yet, as it's about to be updated,
|
||||
// consumers (specifically `BeatmapCarousel.GetSpacingBetweenPanels()`) benefit from `CurrentSelectionItem` already pointing
|
||||
// at the correct item to avoid redundant local equality checks.
|
||||
// the Y positions will be filled in after they're computed.
|
||||
if (isKeyboardSelection)
|
||||
keyboardSelection = new Selection(keyboardSelection.Model, item, null, i);
|
||||
|
||||
if (isSelection)
|
||||
selection = new Selection(selection.Model, item, null, i);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Display handling
|
||||
@@ -1081,7 +1090,7 @@ namespace osu.Game.Graphics.Carousel
|
||||
/// <param name="CarouselItem">A related carousel item representation for the model. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="YPosition">The Y position of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
/// <param name="Index">The index of the selection as of the last run of <see cref="Carousel{T}.refreshAfterSelection"/>. May be null if selection is not present as an item, or if <see cref="Carousel{T}.refreshAfterSelection"/> has not been run yet.</param>
|
||||
private record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
protected record Selection(object? Model = null, CarouselItem? CarouselItem = null, double? YPosition = null, int? Index = null);
|
||||
|
||||
private record DisplayRange(int First, int Last)
|
||||
{
|
||||
|
||||
@@ -15,7 +15,6 @@ using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
{
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public abstract partial class OsuFocusedOverlayContainer : FocusedOverlayContainer, IPreviewTrackOwner, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
protected readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(OverlayActivation.All);
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
public partial class ProgressBar : SliderBar<double>
|
||||
{
|
||||
public bool Seeking { get; private set; }
|
||||
|
||||
public Action<double> OnSeek;
|
||||
|
||||
private readonly Box fill;
|
||||
@@ -75,6 +77,16 @@ namespace osu.Game.Graphics.UserInterface
|
||||
fill.Width = value * UsableWidth;
|
||||
}
|
||||
|
||||
protected override void OnUserChange(double value) => OnSeek?.Invoke(value);
|
||||
protected override void OnUserChange(double value)
|
||||
{
|
||||
Seeking = true;
|
||||
}
|
||||
|
||||
protected override bool Commit()
|
||||
{
|
||||
OnSeek?.Invoke(CurrentNumber.Value);
|
||||
Seeking = false;
|
||||
return base.Commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,13 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
case Key.KeypadEnter:
|
||||
case Key.Enter:
|
||||
return false;
|
||||
// even if committing per se is not allowed for this textbox,
|
||||
// the commit flow is also responsible for terminating any active IME.
|
||||
// ensure that the Enter press terminates IME correctly
|
||||
// and is also handled if it needs to be, so that it doesn't leak to some other non-focused drawable and cause breakage.
|
||||
bool wasImeComposing = ImeCompositionActive;
|
||||
FinalizeImeComposition(true);
|
||||
return wasImeComposing;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,21 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString AdjustBeatmapOffsetAutomaticallyTooltip => new TranslatableString(getKey(@"adjust_beatmap_offset_automatically_tooltip"), @"If enabled, the offset suggested from last play on a beatmap is automatically applied.");
|
||||
|
||||
/// <summary>
|
||||
/// "Use experimental audio mode"
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiLabel => new TranslatableString(getKey(@"wasapi_label"), @"Use experimental audio mode");
|
||||
|
||||
/// <summary>
|
||||
/// "This will attempt to initialise the audio engine in a lower latency mode."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiTooltip => new TranslatableString(getKey(@"wasapi_tooltip"), @"This will attempt to initialise the audio engine in a lower latency mode.");
|
||||
|
||||
/// <summary>
|
||||
/// "Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value."
|
||||
/// </summary>
|
||||
public static LocalisableString WasapiNotice => new TranslatableString(getKey(@"wasapi_notice"), @"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace osu.Game.Online.API.Requests
|
||||
private class VerificationFailureResponse
|
||||
{
|
||||
[JsonProperty("method")]
|
||||
public SessionVerificationMethod RequiredSessionVerificationMethod { get; set; }
|
||||
public SessionVerificationMethod? RequiredSessionVerificationMethod { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@@ -70,6 +71,7 @@ namespace osu.Game.Online.Chat
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly IBindableList<APIRelation> localUserBlocks = new BindableList<APIRelation>();
|
||||
private ScheduledDelegate scheduledAck;
|
||||
|
||||
private IChatClient chatClient = null!;
|
||||
@@ -95,6 +97,9 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
apiState.BindTo(api.State);
|
||||
apiState.BindValueChanged(_ => SendAck(), true);
|
||||
|
||||
localUserBlocks.BindTo(api.LocalUserState.Blocks);
|
||||
localUserBlocks.BindCollectionChanged((_, args) => Schedule(() => onBlocksChanged(args)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -311,8 +316,9 @@ namespace osu.Game.Online.Chat
|
||||
private void addMessages(List<Message> messages)
|
||||
{
|
||||
var channels = JoinedChannels.ToList();
|
||||
var blockedUserIds = localUserBlocks.Select(b => b.TargetID).ToList();
|
||||
|
||||
foreach (var group in messages.GroupBy(m => m.ChannelId))
|
||||
foreach (var group in messages.Where(m => !blockedUserIds.Contains(m.SenderId)).GroupBy(m => m.ChannelId))
|
||||
channels.Find(c => c.Id == group.Key)?.AddNewMessages(group.ToArray());
|
||||
|
||||
lastSilenceMessageId ??= messages.LastOrDefault()?.Id;
|
||||
@@ -641,6 +647,18 @@ namespace osu.Game.Online.Chat
|
||||
api.Queue(req);
|
||||
}
|
||||
|
||||
private void onBlocksChanged(NotifyCollectionChangedEventArgs args)
|
||||
{
|
||||
if (args.Action != NotifyCollectionChangedAction.Add)
|
||||
return;
|
||||
|
||||
foreach (APIRelation newBlock in args.NewItems!)
|
||||
{
|
||||
foreach (var channel in joinedChannels)
|
||||
channel.RemoveMessagesFromUser(newBlock.TargetID);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -43,11 +43,15 @@ namespace osu.Game.Online.Matchmaking
|
||||
/// <summary>
|
||||
/// The user has raised a candidate playlist item to be played.
|
||||
/// </summary>
|
||||
/// <param name="userId">The notifying user.</param>
|
||||
/// <param name="playlistItemId">The playlist item candidate raised, or -1 as a special value that indicates a random selection.</param>
|
||||
Task MatchmakingItemSelected(int userId, long playlistItemId);
|
||||
|
||||
/// <summary>
|
||||
/// The user has removed a candidate playlist item.
|
||||
/// </summary>
|
||||
/// <param name="userId">The notifying user.</param>
|
||||
/// <param name="playlistItemId">The playlist item candidate removed, or -1 as a special value that indicates a random selection.</param>
|
||||
Task MatchmakingItemDeselected(int userId, long playlistItemId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Online.Matchmaking
|
||||
/// <summary>
|
||||
/// Raise a candidate playlist item to be played in the current round.
|
||||
/// </summary>
|
||||
/// <param name="playlistItemId">The playlist item.</param>
|
||||
/// <param name="playlistItemId">The playlist item, or -1 to indicate a random selection.</param>
|
||||
Task MatchmakingToggleSelection(long playlistItemId);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
@@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Notifications
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
IconContent.Width = IconContent.DrawHeight;
|
||||
IconContent.Width = Math.Min(78, IconContent.DrawHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,18 +304,21 @@ namespace osu.Game.Overlays
|
||||
|
||||
var track = musicController.CurrentTrack;
|
||||
|
||||
if (!track.IsDummyDevice)
|
||||
if (!progressBar.Seeking)
|
||||
{
|
||||
progressBar.EndTime = track.Length;
|
||||
progressBar.CurrentTime = track.CurrentTime;
|
||||
if (!track.IsDummyDevice)
|
||||
{
|
||||
progressBar.EndTime = track.Length;
|
||||
progressBar.CurrentTime = track.CurrentTime;
|
||||
|
||||
playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
|
||||
}
|
||||
else
|
||||
{
|
||||
progressBar.CurrentTime = 0;
|
||||
progressBar.EndTime = 1;
|
||||
playButton.Icon = FontAwesome.Regular.PlayCircle;
|
||||
playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
|
||||
}
|
||||
else
|
||||
{
|
||||
progressBar.CurrentTime = 0;
|
||||
progressBar.EndTime = 1;
|
||||
playButton.Icon = FontAwesome.Regular.PlayCircle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@@ -36,6 +38,7 @@ namespace osu.Game.Overlays
|
||||
public ScrollBackButton Button { get; private set; }
|
||||
|
||||
private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>();
|
||||
private readonly Bindable<double> progress = new Bindable<double>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
@@ -46,7 +49,8 @@ namespace osu.Game.Overlays
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(20),
|
||||
Action = scrollBack,
|
||||
LastScrollTarget = { BindTarget = lastScrollTarget }
|
||||
LastScrollTarget = { BindTarget = lastScrollTarget },
|
||||
Progress = { BindTarget = progress },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -54,6 +58,10 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// Map current position to standardized progress
|
||||
float height = AvailableContent - DrawHeight;
|
||||
progress.Value = height == 0 ? 1 : Math.Round(Math.Clamp(Current / height, 0, 1), 3);
|
||||
|
||||
if (ScrollContent.DrawHeight + button_scroll_position < DrawHeight)
|
||||
{
|
||||
Button.State = Visibility.Hidden;
|
||||
@@ -110,9 +118,11 @@ namespace osu.Game.Overlays
|
||||
|
||||
private readonly Container content;
|
||||
private readonly Box background;
|
||||
private readonly CircularProgress currentCircularProgress;
|
||||
private readonly SpriteIcon spriteIcon;
|
||||
|
||||
public Bindable<double?> LastScrollTarget = new Bindable<double?>();
|
||||
public Bindable<double> Progress = new Bindable<double>();
|
||||
|
||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||
|
||||
@@ -145,6 +155,11 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
currentCircularProgress = new CircularProgress
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
InnerRadius = 0.1f,
|
||||
},
|
||||
spriteIcon = new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
@@ -164,6 +179,7 @@ namespace osu.Game.Overlays
|
||||
IdleColour = colourProvider.Background6;
|
||||
HoverColour = colourProvider.Background5;
|
||||
flashColour = colourProvider.Light1;
|
||||
currentCircularProgress.Colour = colourProvider.Highlight1;
|
||||
|
||||
scrollToTopSample = audio.Samples.Get(@"UI/scroll-to-top");
|
||||
scrollToPreviousSample = audio.Samples.Get(@"UI/scroll-to-previous");
|
||||
@@ -173,6 +189,8 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Progress.BindValueChanged(p => currentCircularProgress.Progress = p.NewValue, true);
|
||||
|
||||
LastScrollTarget.BindValueChanged(target =>
|
||||
{
|
||||
spriteIcon.ScaleTo(target.NewValue != null ? new Vector2(1f, -1f) : Vector2.One, fade_duration, Easing.OutQuint);
|
||||
|
||||
@@ -75,19 +75,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
if (percent < 0.0005)
|
||||
return RankingTier.Radiant;
|
||||
|
||||
if (percent < 0.0025)
|
||||
if (percent < 0.0015)
|
||||
return RankingTier.Rhodium;
|
||||
|
||||
if (percent < 0.005)
|
||||
return RankingTier.Platinum;
|
||||
|
||||
if (percent < 0.025)
|
||||
if (percent < 0.015)
|
||||
return RankingTier.Gold;
|
||||
|
||||
if (percent < 0.05)
|
||||
return RankingTier.Silver;
|
||||
|
||||
if (percent < 0.25)
|
||||
if (percent < 0.15)
|
||||
return RankingTier.Bronze;
|
||||
|
||||
if (percent < 0.5)
|
||||
|
||||
@@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
{
|
||||
Add(wasapiExperimental = new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Use experimental audio mode",
|
||||
TooltipText = "This will attempt to initialise the audio engine in a lower latency mode.",
|
||||
LabelText = AudioSettingsStrings.WasapiLabel,
|
||||
TooltipText = AudioSettingsStrings.WasapiTooltip,
|
||||
Current = audio.UseExperimentalWasapi,
|
||||
Keywords = new[] { "wasapi", "latency", "exclusive" }
|
||||
});
|
||||
@@ -64,10 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
if (wasapiExperimental != null)
|
||||
{
|
||||
if (wasapiExperimental.Current.Value)
|
||||
{
|
||||
wasapiExperimental.SetNoticeText(
|
||||
"Due to reduced latency, your audio offset will need to be adjusted when enabling this setting. Generally expect to subtract 20 - 60 ms from your known value.", true);
|
||||
}
|
||||
wasapiExperimental.SetNoticeText(AudioSettingsStrings.WasapiNotice, true);
|
||||
else
|
||||
wasapiExperimental.ClearNoticeText();
|
||||
}
|
||||
|
||||
@@ -550,7 +550,6 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
}
|
||||
else
|
||||
{
|
||||
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
|
||||
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
|
||||
}
|
||||
|
||||
@@ -680,14 +679,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered);
|
||||
}
|
||||
|
||||
private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||
public class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable<FileHitSampleInfo>
|
||||
{
|
||||
public readonly string Filename;
|
||||
|
||||
public FileHitSampleInfo(string filename, int volume)
|
||||
// Force CSS=1 to make sure that the LegacyBeatmapSkin does not fall back to the user skin.
|
||||
// Note that this does not change the lookup names, as they are overridden locally.
|
||||
: base(string.Empty, customSampleBank: 1, volume: volume)
|
||||
: base(HIT_NORMAL, SampleControlPoint.DEFAULT_BANK, customSampleBank: 1, volume: volume)
|
||||
{
|
||||
Filename = filename;
|
||||
}
|
||||
@@ -696,7 +694,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
|
||||
{
|
||||
Filename,
|
||||
Path.ChangeExtension(Filename, null)
|
||||
};
|
||||
}.Concat(base.LookupNames);
|
||||
|
||||
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
|
||||
Optional<bool> newEditorAutoBank = default, Optional<int> newCustomSampleBank = default, Optional<bool> newIsLayered = default)
|
||||
|
||||
@@ -44,7 +44,6 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner, IHandlePresentBeatmap
|
||||
{
|
||||
private readonly Room room;
|
||||
|
||||
@@ -1,466 +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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class BeatmapCardMatchmaking : BeatmapCard
|
||||
{
|
||||
private readonly APIBeatmap beatmap;
|
||||
|
||||
protected override Drawable IdleContent => idleBottomContent;
|
||||
protected override Drawable DownloadInProgressContent => downloadProgressBar;
|
||||
|
||||
public const float HEIGHT = 80;
|
||||
|
||||
[Cached]
|
||||
private readonly BeatmapCardContent content;
|
||||
|
||||
private BeatmapCardThumbnail thumbnail = null!;
|
||||
private CollapsibleButtonContainer buttonContainer = null!;
|
||||
|
||||
private FillFlowContainer idleBottomContent = null!;
|
||||
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
|
||||
|
||||
public AvatarOverlay SelectionOverlay = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
|
||||
|
||||
public BeatmapCardMatchmaking(APIBeatmap beatmap)
|
||||
: base(beatmap.BeatmapSet!, false)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
content = new BeatmapCardContent(HEIGHT);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Width = WIDTH;
|
||||
Height = HEIGHT;
|
||||
|
||||
FillFlowContainer leftIconArea = null!;
|
||||
FillFlowContainer titleBadgeArea = null!;
|
||||
GridContainer artistContainer = null!;
|
||||
|
||||
Child = content.With(c =>
|
||||
{
|
||||
c.MainContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
thumbnail = new BeatmapCardThumbnail(BeatmapSet, BeatmapSet, keepLoaded: true)
|
||||
{
|
||||
Name = @"Left (icon) area",
|
||||
Size = new Vector2(HEIGHT),
|
||||
Padding = new MarginPadding { Right = CORNER_RADIUS },
|
||||
Child = leftIconArea = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding(4),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(1)
|
||||
}
|
||||
},
|
||||
buttonContainer = new CollapsibleButtonContainer(BeatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
|
||||
{
|
||||
X = HEIGHT - CORNER_RADIUS,
|
||||
Width = WIDTH - HEIGHT + CORNER_RADIUS,
|
||||
FavouriteState = { BindTarget = FavouriteState },
|
||||
ButtonsCollapsedWidth = 0,
|
||||
ButtonsExpandedWidth = 24,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
titleBadgeArea = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
artistContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = createArtistText(),
|
||||
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
Empty()
|
||||
},
|
||||
}
|
||||
},
|
||||
new LinkFlowContainer(s =>
|
||||
{
|
||||
s.Shadow = false;
|
||||
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
|
||||
}).With(d =>
|
||||
{
|
||||
d.AutoSizeAxes = Axes.Both;
|
||||
d.Margin = new MarginPadding { Top = 1 };
|
||||
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
|
||||
d.AddUserLink(BeatmapSet.Author);
|
||||
}),
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = @"Bottom content",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
idleBottomContent = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 2),
|
||||
AlwaysPresent = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(4),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(6, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.9f),
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = beatmap.DifficultyName,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
downloadProgressBar = new BeatmapCardDownloadProgressBar
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 5,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
State = { BindTarget = DownloadTracker.State },
|
||||
Progress = { BindTarget = DownloadTracker.Progress }
|
||||
}
|
||||
}
|
||||
},
|
||||
SelectionOverlay = new AvatarOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
c.Expanded.BindTarget = Expanded;
|
||||
});
|
||||
|
||||
if (BeatmapSet.HasVideo)
|
||||
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (BeatmapSet.HasStoryboard)
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (BeatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
titleBadgeArea.Add(new SpotlightBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (BeatmapSet.HasExplicitContent)
|
||||
{
|
||||
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (BeatmapSet.TrackId != null)
|
||||
{
|
||||
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private LocalisableString createArtistText()
|
||||
{
|
||||
var romanisableArtist = new RomanisableString(BeatmapSet.ArtistUnicode, BeatmapSet.Artist);
|
||||
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
|
||||
}
|
||||
|
||||
protected override void UpdateState()
|
||||
{
|
||||
base.UpdateState();
|
||||
|
||||
bool showDetails = IsHovered;
|
||||
|
||||
buttonContainer.ShowDetails.Value = showDetails;
|
||||
thumbnail.Dimmed.Value = showDetails;
|
||||
}
|
||||
|
||||
public override MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
List<MenuItem> items = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
|
||||
};
|
||||
|
||||
foreach (var button in buttonContainer.Buttons)
|
||||
{
|
||||
if (button.Enabled.Value)
|
||||
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class AvatarOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly Container<SelectionAvatar> avatars;
|
||||
|
||||
private Sample? userAddedSample;
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public AvatarOverlay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = avatars = new Container<SelectionAvatar>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = SelectionAvatar.AVATAR_SIZE,
|
||||
};
|
||||
|
||||
Padding = new MarginPadding { Vertical = 5 };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user)
|
||||
{
|
||||
if (avatars.Any(a => a.User.Id == user.Id))
|
||||
return false;
|
||||
|
||||
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
|
||||
|
||||
avatars.Add(avatar);
|
||||
|
||||
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
||||
{
|
||||
userAddedSample?.Play();
|
||||
lastSamplePlayback = Time.Current;
|
||||
}
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
avatar.FinishTransforms();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveUser(int id)
|
||||
{
|
||||
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
|
||||
return false;
|
||||
|
||||
avatar.PopOutAndExpire();
|
||||
avatars.ChangeChildDepth(avatar, float.MaxValue);
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateAvatarLayout()
|
||||
{
|
||||
const double stagger = 30;
|
||||
const float spacing = 4;
|
||||
|
||||
double delay = 0;
|
||||
float x = 0;
|
||||
|
||||
for (int i = avatars.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var avatar = avatars[i];
|
||||
|
||||
if (avatar.Expired)
|
||||
continue;
|
||||
|
||||
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
|
||||
|
||||
x -= avatar.LayoutSize.X + spacing;
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SelectionAvatar : CompositeDrawable
|
||||
{
|
||||
public const float AVATAR_SIZE = 30;
|
||||
|
||||
public APIUser User { get; }
|
||||
|
||||
public bool Expired { get; private set; }
|
||||
|
||||
private readonly MatchmakingAvatar avatar;
|
||||
|
||||
public SelectionAvatar(APIUser user, bool isOwnUser)
|
||||
{
|
||||
User = user;
|
||||
Size = new Vector2(AVATAR_SIZE);
|
||||
|
||||
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
avatar.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire()
|
||||
{
|
||||
avatar.ScaleTo(0, 400, Easing.OutExpo);
|
||||
|
||||
this.FadeOut(100).Expire();
|
||||
Expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Toolkit.HighPerformance;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@@ -33,16 +34,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
public event Action<MultiplayerPlaylistItem>? ItemSelected;
|
||||
|
||||
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>();
|
||||
private readonly Dictionary<long, MatchmakingSelectPanel> panelLookup = new Dictionary<long, MatchmakingSelectPanel>();
|
||||
private readonly Dictionary<long, MatchmakingPlaylistItem> playlistItems = new Dictionary<long, MatchmakingPlaylistItem>();
|
||||
private MatchmakingSelectPanelRandom randomPanel = null!;
|
||||
|
||||
private readonly PanelGridContainer panelGridContainer;
|
||||
private readonly Container<BeatmapSelectPanel> rollContainer;
|
||||
private readonly Container<MatchmakingSelectPanel> rollContainer;
|
||||
private readonly OsuScrollContainer scroll;
|
||||
|
||||
private bool allowSelection = true;
|
||||
|
||||
private readonly Sample?[] spinSamples = new Sample?[5];
|
||||
private static readonly int[] spin_sample_sequence = [0, 1, 2, 3, 4, 2, 3, 4];
|
||||
private Sample? randomRevealSample;
|
||||
private Sample? resultSample;
|
||||
private Sample? swooshSample;
|
||||
private double? lastSamplePlayback;
|
||||
@@ -63,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
Spacing = new Vector2(panel_spacing)
|
||||
},
|
||||
},
|
||||
rollContainer = new Container<BeatmapSelectPanel>
|
||||
rollContainer = new Container<MatchmakingSelectPanel>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
@@ -77,13 +81,38 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
for (int i = 0; i < spinSamples.Length; i++)
|
||||
spinSamples[i] = audio.Samples.Get($@"Multiplayer/Matchmaking/Selection/roulette-{i}");
|
||||
|
||||
randomRevealSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/random-reveal");
|
||||
resultSample = audio.Samples.Get(@"Multiplayer/Matchmaking/Selection/roulette-result");
|
||||
swooshSample = audio.Samples.Get(@"SongSelect/options-pop-out");
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
public void AddItems(IEnumerable<MatchmakingPlaylistItem> items)
|
||||
{
|
||||
base.LoadComplete();
|
||||
foreach (var item in items)
|
||||
{
|
||||
playlistItems[item.ID] = item;
|
||||
|
||||
var panel = panelLookup[item.ID] = new MatchmakingSelectPanelBeatmap(item)
|
||||
{
|
||||
AllowSelection = allowSelection,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = i => ItemSelected?.Invoke(i),
|
||||
};
|
||||
|
||||
panelGridContainer.Add(panel);
|
||||
panelGridContainer.SetLayoutPosition(panel, (float)panel.Item.StarRating);
|
||||
}
|
||||
|
||||
panelLookup[-1] = randomPanel = new MatchmakingSelectPanelRandom(new MultiplayerPlaylistItem { ID = -1 })
|
||||
{
|
||||
AllowSelection = allowSelection,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = i => ItemSelected?.Invoke(i),
|
||||
};
|
||||
panelGridContainer.Add(randomPanel);
|
||||
panelGridContainer.SetLayoutPosition(randomPanel, float.MinValue);
|
||||
|
||||
const double enter_duration = 500;
|
||||
|
||||
@@ -99,32 +128,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
panel.FadeInAndEnterFromBelow(duration: enter_duration, delay: delay);
|
||||
}
|
||||
|
||||
panelsLoaded.SetResult();
|
||||
});
|
||||
}
|
||||
|
||||
public void AddItem(MultiplayerPlaylistItem item)
|
||||
{
|
||||
var panel = panelLookup[item.ID] = new BeatmapSelectPanel(item)
|
||||
{
|
||||
AllowSelection = allowSelection,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = ItemSelected,
|
||||
};
|
||||
|
||||
panelGridContainer.Add(panel);
|
||||
panelGridContainer.SetLayoutPosition(panel, (float)item.StarRating);
|
||||
}
|
||||
|
||||
public void RemoveItem(long id)
|
||||
{
|
||||
if (!panelLookup.Remove(id, out var panel))
|
||||
return;
|
||||
|
||||
panel.Expire();
|
||||
}
|
||||
|
||||
public void SetUserSelection(APIUser user, long itemId, bool selected)
|
||||
public void SetUserSelection(APIUser user, long itemId, bool selected) => whenPanelsLoaded(() =>
|
||||
{
|
||||
if (!panelLookup.TryGetValue(itemId, out var panel))
|
||||
return;
|
||||
@@ -133,9 +142,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
panel.AddUser(user);
|
||||
else
|
||||
panel.RemoveUser(user);
|
||||
}
|
||||
});
|
||||
|
||||
public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId)
|
||||
public void RevealRandomItem(MultiplayerPlaylistItem item) => whenPanelsLoaded(() =>
|
||||
{
|
||||
playlistItems.TryGetValue(item.ID, out var playlistItem);
|
||||
|
||||
Debug.Assert(playlistItem != null);
|
||||
|
||||
randomRevealSample?.Play();
|
||||
randomPanel.RevealBeatmap(playlistItem.Beatmap, playlistItem.Mods);
|
||||
});
|
||||
|
||||
public void RollAndDisplayFinalBeatmap(long[] candidateItemIds, long finalItemId) => whenPanelsLoaded(() =>
|
||||
{
|
||||
Debug.Assert(candidateItemIds.Length >= 1);
|
||||
Debug.Assert(candidateItemIds.Contains(finalItemId));
|
||||
@@ -162,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
.Delay(roll_duration + present_beatmap_delay)
|
||||
.Schedule(() => PresentRolledBeatmap(finalItemId));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
internal void TransferCandidatePanelsToRollContainer(long[] candidateItemIds, double duration = hide_duration)
|
||||
{
|
||||
@@ -171,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
var rng = new Random();
|
||||
|
||||
var remainingPanels = new List<BeatmapSelectPanel>();
|
||||
var remainingPanels = new List<MatchmakingSelectPanel>();
|
||||
|
||||
foreach (var panel in panelGridContainer.Children.ToArray())
|
||||
{
|
||||
@@ -211,7 +230,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
var panel = rollContainer.Children[i];
|
||||
|
||||
var position = positions[i] * (BeatmapSelectPanel.SIZE + new Vector2(panel_spacing));
|
||||
var position = positions[i] * (MatchmakingSelectPanel.SIZE + new Vector2(panel_spacing));
|
||||
|
||||
panel.MoveTo(position, duration + stagger * i, new SplitEasingFunction(Easing.InCubic, Easing.OutExpo, 0.3f));
|
||||
|
||||
@@ -280,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
while ((numSteps - 1) % rollContainer.Children.Count != finalItemIndex)
|
||||
numSteps++;
|
||||
|
||||
BeatmapSelectPanel? lastPanel = null;
|
||||
MatchmakingSelectPanel? lastPanel = null;
|
||||
|
||||
for (int i = 0; i < numSteps; i++)
|
||||
{
|
||||
@@ -341,7 +360,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
PresentRolledBeatmap(finalItem);
|
||||
}
|
||||
|
||||
private partial class PanelGridContainer : FillFlowContainer<BeatmapSelectPanel>
|
||||
private readonly TaskCompletionSource panelsLoaded = new TaskCompletionSource();
|
||||
|
||||
private void whenPanelsLoaded(Action action) => Task.Run(async () =>
|
||||
{
|
||||
await panelsLoaded.Task.ConfigureAwait(false);
|
||||
Schedule(action);
|
||||
});
|
||||
|
||||
private partial class PanelGridContainer : FillFlowContainer<MatchmakingSelectPanel>
|
||||
{
|
||||
public bool LayoutDisabled;
|
||||
|
||||
|
||||
@@ -1,241 +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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class BeatmapSelectPanel : Container
|
||||
{
|
||||
public static readonly Vector2 SIZE = new Vector2(BeatmapCard.WIDTH, BeatmapCardNormal.HEIGHT);
|
||||
|
||||
public bool AllowSelection { get; set; }
|
||||
|
||||
public readonly MultiplayerPlaylistItem Item;
|
||||
|
||||
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
|
||||
|
||||
private const float border_width = 3;
|
||||
|
||||
private Container scaleContainer = null!;
|
||||
private Drawable lighting = null!;
|
||||
|
||||
private Container border = null!;
|
||||
private Container mainContent = null!;
|
||||
|
||||
private readonly List<APIUser> users = new List<APIUser>();
|
||||
|
||||
private BeatmapCardMatchmaking? card;
|
||||
|
||||
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Item = item;
|
||||
Size = SIZE;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(BeatmapLookupCache lookupCache, OverlayColourProvider colourProvider)
|
||||
{
|
||||
InternalChild = scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
mainContent = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
lighting = new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
border = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderThickness = border_width,
|
||||
BorderColour = colourProvider.Light1,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 40,
|
||||
Roundness = 300,
|
||||
Colour = colourProvider.Light3.Opacity(0.1f),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
|
||||
{
|
||||
Debug.Assert(card == null);
|
||||
|
||||
APIBeatmap beatmap = b.GetResultSafely() ?? new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "unknown beatmap",
|
||||
TitleUnicode = "unknown beatmap",
|
||||
Artist = "unknown artist",
|
||||
ArtistUnicode = "unknown artist",
|
||||
}
|
||||
};
|
||||
|
||||
beatmap.StarRating = Item.StarRating;
|
||||
|
||||
mainContent.Add(card = new BeatmapCardMatchmaking(beatmap)
|
||||
{
|
||||
Depth = float.MaxValue,
|
||||
Action = () =>
|
||||
{
|
||||
if (AllowSelection)
|
||||
Action?.Invoke(Item);
|
||||
},
|
||||
});
|
||||
|
||||
foreach (var user in users)
|
||||
card.SelectionOverlay.AddUser(user);
|
||||
}));
|
||||
}
|
||||
|
||||
public void AddUser(APIUser user)
|
||||
{
|
||||
users.Add(user);
|
||||
card?.SelectionOverlay.AddUser(user);
|
||||
}
|
||||
|
||||
public void RemoveUser(APIUser user)
|
||||
{
|
||||
users.Remove(user);
|
||||
card?.SelectionOverlay.RemoveUser(user.Id);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
|
||||
lighting.FadeOut(200);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (AllowSelection && e.Button == MouseButton.Left)
|
||||
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
}
|
||||
|
||||
// pass through to let the beatmap card handle actual click.
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ShowChosenBorder()
|
||||
{
|
||||
border.FadeTo(1, 1000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void ShowBorder()
|
||||
{
|
||||
border.FadeTo(1, 80, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeTo(0.7f, 800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void HideBorder()
|
||||
{
|
||||
border.FadeOut(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
|
||||
{
|
||||
scaleContainer
|
||||
.FadeOut()
|
||||
.MoveToY(distance)
|
||||
.Delay(delay)
|
||||
.FadeIn(duration / 2)
|
||||
.MoveToY(0, duration, Easing.OutExpo);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
|
||||
{
|
||||
AllowSelection = false;
|
||||
|
||||
scaleContainer.Delay(delay)
|
||||
.ScaleTo(0, duration, easing)
|
||||
.FadeOut(duration);
|
||||
|
||||
this.Delay(delay + duration).FadeOut().Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public record MatchmakingPlaylistItem(MultiplayerPlaylistItem PlaylistItem, APIBeatmap Beatmap, Mod[] Mods)
|
||||
{
|
||||
public long ID => PlaylistItem.ID;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class MatchmakingSelectPanel
|
||||
{
|
||||
public abstract partial class CardContent : CompositeDrawable
|
||||
{
|
||||
public abstract AvatarOverlay SelectionOverlay { get; }
|
||||
|
||||
protected CardContent()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
|
||||
public partial class AvatarOverlay : CompositeDrawable
|
||||
{
|
||||
private readonly Container<SelectionAvatar> avatars;
|
||||
|
||||
private Sample? userAddedSample;
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
public AvatarOverlay()
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = avatars = new Container<SelectionAvatar>
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = SelectionAvatar.AVATAR_SIZE,
|
||||
};
|
||||
|
||||
Padding = new MarginPadding { Vertical = 5 };
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
|
||||
}
|
||||
|
||||
public bool AddUser(APIUser user)
|
||||
{
|
||||
if (avatars.Any(a => a.User.Id == user.Id))
|
||||
return false;
|
||||
|
||||
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
|
||||
|
||||
avatars.Add(avatar);
|
||||
|
||||
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
|
||||
{
|
||||
userAddedSample?.Play();
|
||||
lastSamplePlayback = Time.Current;
|
||||
}
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
avatar.FinishTransforms();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveUser(int id)
|
||||
{
|
||||
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
|
||||
return false;
|
||||
|
||||
avatar.PopOutAndExpire();
|
||||
avatars.ChangeChildDepth(avatar, float.MaxValue);
|
||||
|
||||
updateAvatarLayout();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateAvatarLayout()
|
||||
{
|
||||
const double stagger = 30;
|
||||
const float spacing = 4;
|
||||
|
||||
double delay = 0;
|
||||
float x = 0;
|
||||
|
||||
for (int i = avatars.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var avatar = avatars[i];
|
||||
|
||||
if (avatar.Expired)
|
||||
continue;
|
||||
|
||||
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
|
||||
|
||||
x -= avatar.LayoutSize.X + spacing;
|
||||
|
||||
delay += stagger;
|
||||
}
|
||||
}
|
||||
|
||||
public partial class SelectionAvatar : CompositeDrawable
|
||||
{
|
||||
public const float AVATAR_SIZE = 30;
|
||||
|
||||
public APIUser User { get; }
|
||||
|
||||
public bool Expired { get; private set; }
|
||||
|
||||
private readonly MatchmakingAvatar avatar;
|
||||
|
||||
public SelectionAvatar(APIUser user, bool isOwnUser)
|
||||
{
|
||||
User = user;
|
||||
Size = new Vector2(AVATAR_SIZE);
|
||||
|
||||
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
avatar.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire()
|
||||
{
|
||||
avatar.ScaleTo(0, 400, Easing.OutExpo);
|
||||
|
||||
this.FadeOut(100).Expire();
|
||||
Expired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// 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.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapSet;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class MatchmakingSelectPanel
|
||||
{
|
||||
public partial class CardContentBeatmap : CardContent, IHasContextMenu
|
||||
{
|
||||
public override AvatarOverlay SelectionOverlay => selectionOverlay;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapSetOverlay? beatmapSetOverlay { get; set; }
|
||||
|
||||
private readonly IBindable<DownloadState> downloadState = new Bindable<DownloadState>();
|
||||
private readonly IBindableNumber<double> downloadProgress = new BindableDouble();
|
||||
private readonly Bindable<BeatmapSetFavouriteState> favouriteState = new Bindable<BeatmapSetFavouriteState>();
|
||||
private readonly APIBeatmapSet beatmapSet;
|
||||
private readonly APIBeatmap beatmap;
|
||||
private readonly Mod[] mods;
|
||||
|
||||
private BeatmapCardThumbnail thumbnail = null!;
|
||||
private CollapsibleButtonContainer buttonContainer = null!;
|
||||
private FillFlowContainer idleBottomContent = null!;
|
||||
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
|
||||
private AvatarOverlay selectionOverlay = null!;
|
||||
|
||||
public CardContentBeatmap(APIBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
this.beatmap = beatmap;
|
||||
this.mods = mods;
|
||||
|
||||
beatmapSet = beatmap.BeatmapSet!;
|
||||
favouriteState.Value = new BeatmapSetFavouriteState(beatmapSet.HasFavourited, beatmapSet.FavouriteCount);
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
FillFlowContainer leftIconArea;
|
||||
FillFlowContainer titleBadgeArea;
|
||||
GridContainer artistContainer;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new BeatmapDownloadTracker(beatmap.BeatmapSet!)
|
||||
{
|
||||
State = { BindTarget = downloadState },
|
||||
Progress = { BindTarget = downloadProgress },
|
||||
},
|
||||
thumbnail = new BeatmapCardThumbnail(beatmapSet, beatmapSet, keepLoaded: true)
|
||||
{
|
||||
Name = @"Left (icon) area",
|
||||
Size = new Vector2(MatchmakingSelectPanel.HEIGHT),
|
||||
Padding = new MarginPadding { Right = BeatmapCard.CORNER_RADIUS },
|
||||
Child = leftIconArea = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding(4),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(1)
|
||||
}
|
||||
},
|
||||
buttonContainer = new CollapsibleButtonContainer(beatmapSet, allowNavigationToBeatmap: false, keepBackgroundLoaded: true)
|
||||
{
|
||||
X = MatchmakingSelectPanel.HEIGHT - BeatmapCard.CORNER_RADIUS,
|
||||
Width = BeatmapCard.WIDTH - MatchmakingSelectPanel.HEIGHT + BeatmapCard.CORNER_RADIUS,
|
||||
FavouriteState = { BindTarget = favouriteState },
|
||||
ButtonsCollapsedWidth = 0,
|
||||
ButtonsExpandedWidth = 24,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
|
||||
Font = OsuFont.Default.With(size: 18f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
titleBadgeArea = new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
artistContainer = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new[]
|
||||
{
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = BeatmapsetsStrings.ShowDetailsByArtist(new RomanisableString(beatmapSet.ArtistUnicode, beatmapSet.Artist)),
|
||||
Font = OsuFont.Default.With(size: 14f, weight: FontWeight.SemiBold),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
},
|
||||
Empty()
|
||||
},
|
||||
}
|
||||
},
|
||||
new LinkFlowContainer(s =>
|
||||
{
|
||||
s.Shadow = false;
|
||||
s.Font = OsuFont.GetFont(size: 11f, weight: FontWeight.SemiBold);
|
||||
}).With(d =>
|
||||
{
|
||||
d.AutoSizeAxes = Axes.Both;
|
||||
d.Margin = new MarginPadding { Top = 1 };
|
||||
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
|
||||
d.AddUserLink(beatmapSet.Author);
|
||||
}),
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Name = @"Bottom content",
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
idleBottomContent = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 2),
|
||||
AlwaysPresent = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.ForStarDifficulty(beatmap.StarRating).Darken(0.8f),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(4),
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(6, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
|
||||
{
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Scale = new Vector2(0.9f),
|
||||
},
|
||||
new TruncatingSpriteText
|
||||
{
|
||||
Text = beatmap.DifficultyName,
|
||||
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
new ModFlowDisplay
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Scale = new Vector2(0.5f),
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Current = { Value = mods }
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
downloadProgressBar = new BeatmapCardDownloadProgressBar
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 5,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
State = { BindTarget = downloadState },
|
||||
Progress = { BindTarget = downloadProgress }
|
||||
}
|
||||
}
|
||||
},
|
||||
selectionOverlay = new AvatarOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (beatmapSet.HasVideo)
|
||||
leftIconArea.Add(new VideoIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (beatmapSet.HasStoryboard)
|
||||
leftIconArea.Add(new StoryboardIconPill { IconSize = new Vector2(16) });
|
||||
|
||||
if (beatmapSet.FeaturedInSpotlight)
|
||||
{
|
||||
titleBadgeArea.Add(new SpotlightBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (beatmapSet.HasExplicitContent)
|
||||
{
|
||||
titleBadgeArea.Add(new ExplicitContentBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
});
|
||||
}
|
||||
|
||||
if (beatmapSet.TrackId != null)
|
||||
{
|
||||
artistContainer.Content[0][1] = new FeaturedArtistBeatmapBadge
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding { Left = 4 }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
downloadState.BindValueChanged(_ => updateState(), true);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
bool showDetails = IsHovered;
|
||||
|
||||
buttonContainer.ShowDetails.Value = showDetails;
|
||||
thumbnail.Dimmed.Value = showDetails;
|
||||
|
||||
bool showProgress = downloadState.Value == DownloadState.Downloading || downloadState.Value == DownloadState.Importing;
|
||||
|
||||
idleBottomContent.FadeTo(showProgress ? 0 : 1, 340, Easing.OutQuint);
|
||||
downloadProgressBar.FadeTo(showProgress ? 1 : 0, 340, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public MenuItem[] ContextMenuItems
|
||||
{
|
||||
get
|
||||
{
|
||||
List<MenuItem> items = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, () => beatmapSetOverlay?.FetchAndShowBeatmap(beatmap.OnlineID))
|
||||
};
|
||||
|
||||
foreach (var button in buttonContainer.Buttons)
|
||||
{
|
||||
if (button.Enabled.Value)
|
||||
items.Add(new OsuMenuItem(button.TooltipText.ToSentence(), MenuItemType.Standard, () => button.TriggerClick()));
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class MatchmakingSelectPanel
|
||||
{
|
||||
public partial class CardContentRandom : CardContent
|
||||
{
|
||||
public override AvatarOverlay SelectionOverlay => selectionOverlay;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
private AvatarOverlay selectionOverlay = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background2,
|
||||
},
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Horizontal = 10,
|
||||
Vertical = 4
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children =
|
||||
[
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Size = new Vector2(32),
|
||||
Icon = FontAwesome.Solid.Random,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Text = "Random",
|
||||
}
|
||||
]
|
||||
},
|
||||
selectionOverlay = new AvatarOverlay
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.Drawables.Cards;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public abstract partial class MatchmakingSelectPanel : Container
|
||||
{
|
||||
public const float WIDTH = 345;
|
||||
public const float HEIGHT = 80;
|
||||
|
||||
public static readonly Vector2 SIZE = new Vector2(WIDTH, HEIGHT);
|
||||
|
||||
public bool AllowSelection { get; set; }
|
||||
|
||||
public readonly MultiplayerPlaylistItem Item;
|
||||
|
||||
public Action<MultiplayerPlaylistItem>? Action { private get; init; }
|
||||
|
||||
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
private const float border_width = 3;
|
||||
|
||||
private Container scaleContainer = null!;
|
||||
private Drawable lighting = null!;
|
||||
private Container border = null!;
|
||||
|
||||
protected MatchmakingSelectPanel(MultiplayerPlaylistItem item)
|
||||
{
|
||||
Item = item;
|
||||
Size = SIZE;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
scaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
Content,
|
||||
lighting = new Box
|
||||
{
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
}
|
||||
},
|
||||
border = new Container
|
||||
{
|
||||
Alpha = 0,
|
||||
Masking = true,
|
||||
CornerRadius = BeatmapCard.CORNER_RADIUS,
|
||||
CornerExponent = 10,
|
||||
Blending = BlendingParameters.Additive,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
BorderThickness = border_width,
|
||||
BorderColour = colourProvider.Light1,
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Glow,
|
||||
Radius = 40,
|
||||
Roundness = 300,
|
||||
Colour = colourProvider.Light3.Opacity(0.1f),
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
Colour = Color4.Black,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
new HoverClickSounds(),
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: making these abstract for now but avatar overlay should really be owned by the top level class
|
||||
public abstract void AddUser(APIUser user);
|
||||
|
||||
public abstract void RemoveUser(APIUser user);
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.2f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 300);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
base.OnHoverLost(e);
|
||||
|
||||
lighting.FadeOut(200);
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e)
|
||||
{
|
||||
if (AllowSelection && e.Button == MouseButton.Left)
|
||||
scaleContainer.ScaleTo(0.95f, 400, Easing.OutExpo);
|
||||
|
||||
return base.OnMouseDown(e);
|
||||
}
|
||||
|
||||
protected override void OnMouseUp(MouseUpEvent e)
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
|
||||
if (e.Button == MouseButton.Left)
|
||||
scaleContainer.ScaleTo(1f, 500, Easing.OutElasticHalf);
|
||||
}
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (AllowSelection)
|
||||
{
|
||||
lighting.FadeTo(0.5f, 50)
|
||||
.Then()
|
||||
.FadeTo(0.1f, 400);
|
||||
|
||||
Action?.Invoke(Item);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ShowChosenBorder()
|
||||
{
|
||||
border.FadeTo(1, 1000, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void ShowBorder()
|
||||
{
|
||||
border.FadeTo(1, 80, Easing.OutQuint)
|
||||
.Then()
|
||||
.FadeTo(0.7f, 800, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void HideBorder()
|
||||
{
|
||||
border.FadeOut(500, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public void FadeInAndEnterFromBelow(double duration = 500, double delay = 0, float distance = 200)
|
||||
{
|
||||
scaleContainer
|
||||
.FadeOut()
|
||||
.MoveToY(distance)
|
||||
.Delay(delay)
|
||||
.FadeIn(duration / 2)
|
||||
.MoveToY(0, duration, Easing.OutExpo);
|
||||
}
|
||||
|
||||
public void PopOutAndExpire(double duration = 400, double delay = 0, Easing easing = Easing.InCubic)
|
||||
{
|
||||
AllowSelection = false;
|
||||
|
||||
scaleContainer.Delay(delay)
|
||||
.ScaleTo(0, duration, easing)
|
||||
.FadeOut(duration);
|
||||
|
||||
this.Delay(delay + duration).FadeOut().Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class MatchmakingSelectPanelBeatmap : MatchmakingSelectPanel
|
||||
{
|
||||
private readonly APIBeatmap beatmap;
|
||||
private readonly Mod[] mods;
|
||||
|
||||
public MatchmakingSelectPanelBeatmap(MatchmakingPlaylistItem item)
|
||||
: base(item.PlaylistItem)
|
||||
{
|
||||
beatmap = item.Beatmap;
|
||||
mods = item.Mods;
|
||||
}
|
||||
|
||||
private CardContent content = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(content = new CardContentBeatmap(beatmap, mods));
|
||||
}
|
||||
|
||||
public override void AddUser(APIUser user)
|
||||
{
|
||||
content.SelectionOverlay.AddUser(user);
|
||||
}
|
||||
|
||||
public override void RemoveUser(APIUser user)
|
||||
{
|
||||
content.SelectionOverlay.RemoveUser(user.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
public partial class MatchmakingSelectPanelRandom : MatchmakingSelectPanel
|
||||
{
|
||||
public MatchmakingSelectPanelRandom(MultiplayerPlaylistItem item)
|
||||
: base(item)
|
||||
{
|
||||
}
|
||||
|
||||
private CardContent content = null!;
|
||||
private readonly List<APIUser> users = new List<APIUser>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Add(content = new CardContentRandom());
|
||||
}
|
||||
|
||||
public void RevealBeatmap(APIBeatmap beatmap, Mod[] mods)
|
||||
{
|
||||
content.Expire();
|
||||
|
||||
var flashLayer = new Box { RelativeSizeAxes = Axes.Both };
|
||||
|
||||
AddRange(new Drawable[]
|
||||
{
|
||||
content = new CardContentBeatmap(beatmap, mods),
|
||||
flashLayer,
|
||||
});
|
||||
|
||||
foreach (var user in users)
|
||||
content.SelectionOverlay.AddUser(user);
|
||||
|
||||
flashLayer.FadeOutFromOne(1000, Easing.In);
|
||||
}
|
||||
|
||||
public override void AddUser(APIUser user)
|
||||
{
|
||||
users.Add(user);
|
||||
content.SelectionOverlay.AddUser(user);
|
||||
}
|
||||
|
||||
public override void RemoveUser(APIUser user)
|
||||
{
|
||||
users.Remove(user);
|
||||
content.SelectionOverlay.RemoveUser(user.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
@@ -17,10 +27,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
public override Drawable PlayersDisplayArea { get; }
|
||||
|
||||
private readonly BeatmapSelectGrid beatmapSelectGrid;
|
||||
private readonly LoadingSpinner loadingSpinner;
|
||||
|
||||
[Resolved]
|
||||
private MultiplayerClient client { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapLookupCache beatmapLookupCache { get; set; } = null!;
|
||||
|
||||
public SubScreenBeatmapSelect()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
@@ -29,9 +46,19 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Horizontal = 200 },
|
||||
Child = beatmapSelectGrid = new BeatmapSelectGrid
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
beatmapSelectGrid = new BeatmapSelectGrid
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
loadingSpinner = new LoadingSpinner
|
||||
{
|
||||
Size = new Vector2(64),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
State = { Value = Visibility.Visible }
|
||||
}
|
||||
},
|
||||
},
|
||||
new Container
|
||||
@@ -49,24 +76,53 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
client.ItemAdded += onItemAdded;
|
||||
|
||||
foreach (var item in client.Room!.Playlist)
|
||||
onItemAdded(item);
|
||||
|
||||
beatmapSelectGrid.ItemSelected += item => client.MatchmakingToggleSelection(item.ID);
|
||||
|
||||
client.MatchmakingItemSelected += onItemSelected;
|
||||
client.MatchmakingItemDeselected += onItemDeselected;
|
||||
client.SettingsChanged += onSettingsChanged;
|
||||
|
||||
Debug.Assert(client.Room != null);
|
||||
|
||||
loadItems(client.Room.Playlist.ToArray()).FireAndForget();
|
||||
}
|
||||
|
||||
private void onItemAdded(MultiplayerPlaylistItem item) => Scheduler.Add(() =>
|
||||
private async Task loadItems(MultiplayerPlaylistItem[] items)
|
||||
{
|
||||
if (item.Expired)
|
||||
return;
|
||||
var beatmaps = await beatmapLookupCache.GetBeatmapsAsync(items.Select(it => it.BeatmapID).ToArray()).ConfigureAwait(false);
|
||||
var matchmakingItems = new List<MatchmakingPlaylistItem>();
|
||||
|
||||
beatmapSelectGrid.AddItem(item);
|
||||
});
|
||||
foreach (var entry in items.Zip(beatmaps))
|
||||
{
|
||||
var (item, beatmap) = entry;
|
||||
|
||||
beatmap ??= new APIBeatmap
|
||||
{
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = "unknown beatmap",
|
||||
TitleUnicode = "unknown beatmap",
|
||||
Artist = "unknown artist",
|
||||
ArtistUnicode = "unknown artist",
|
||||
}
|
||||
};
|
||||
|
||||
beatmap.StarRating = item.StarRating;
|
||||
|
||||
Ruleset? ruleset = rulesetStore.GetRuleset(item.RulesetID)?.CreateInstance();
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
|
||||
Mod[] mods = item.RequiredMods.Select(m => m.ToMod(ruleset)).ToArray();
|
||||
|
||||
matchmakingItems.Add(new MatchmakingPlaylistItem(item, beatmap, mods));
|
||||
}
|
||||
|
||||
Scheduler.Add(() =>
|
||||
{
|
||||
loadingSpinner.Hide();
|
||||
beatmapSelectGrid.AddItems(matchmakingItems);
|
||||
});
|
||||
}
|
||||
|
||||
private void onItemSelected(int userId, long itemId)
|
||||
{
|
||||
@@ -80,6 +136,20 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
beatmapSelectGrid.SetUserSelection(user, itemId, false);
|
||||
}
|
||||
|
||||
private void onSettingsChanged(MultiplayerRoomSettings settings)
|
||||
{
|
||||
if (client.Room!.MatchState is not MatchmakingRoomState matchmakingState)
|
||||
return;
|
||||
|
||||
if (matchmakingState.Stage != MatchmakingStage.ServerBeatmapFinalised)
|
||||
return;
|
||||
|
||||
if (matchmakingState.CandidateItem != -1)
|
||||
return;
|
||||
|
||||
beatmapSelectGrid.RevealRandomItem(client.Room!.CurrentPlaylistItem);
|
||||
}
|
||||
|
||||
public void RollFinalBeatmap(long[] candidateItems, long finalItem) => beatmapSelectGrid.RollAndDisplayFinalBeatmap(candidateItems, finalItem);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
@@ -88,9 +158,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
|
||||
|
||||
if (client.IsNotNull())
|
||||
{
|
||||
client.ItemAdded -= onItemAdded;
|
||||
client.MatchmakingItemSelected -= onItemSelected;
|
||||
client.MatchmakingItemDeselected -= onItemDeselected;
|
||||
client.SettingsChanged -= onSettingsChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ using System.Globalization;
|
||||
using System.Linq;
|
||||
using Humanizer;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@@ -15,6 +17,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@@ -114,6 +117,18 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
|
||||
private bool hasQuit;
|
||||
|
||||
private enum InteractionSampleType
|
||||
{
|
||||
PlayerJump,
|
||||
PlayerReJump,
|
||||
OtherPlayerJump,
|
||||
}
|
||||
|
||||
private Dictionary<InteractionSampleType, Sample?> interactionSamples = new Dictionary<InteractionSampleType, Sample?>();
|
||||
private readonly Dictionary<InteractionSampleType, SampleChannel?> interactionSampleChannels = new Dictionary<InteractionSampleType, SampleChannel?>();
|
||||
private double samplePitch;
|
||||
private double? lastSamplePlayback;
|
||||
|
||||
public PlayerPanel(MultiplayerRoomUser user)
|
||||
: base(HoverSampleSet.Button)
|
||||
{
|
||||
@@ -130,7 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
private void load(AudioManager audio)
|
||||
{
|
||||
Content.Masking = true;
|
||||
Content.CornerRadius = 10;
|
||||
@@ -255,6 +270,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
// Allow avatar to exist outside of masking for when it jumps around and stuff.
|
||||
AddInternal(avatar.CreateProxy());
|
||||
|
||||
interactionSamples = new Dictionary<InteractionSampleType, Sample?>
|
||||
{
|
||||
{ InteractionSampleType.PlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump") },
|
||||
{ InteractionSampleType.PlayerReJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-rejump") },
|
||||
{ InteractionSampleType.OtherPlayerJump, audio.Samples.Get(@"Multiplayer/Matchmaking/player-jump-other") }
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@@ -272,6 +294,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
avatar.ScaleTo(0)
|
||||
.ScaleTo(1, 500, Easing.OutElasticHalf)
|
||||
.FadeIn(200);
|
||||
|
||||
// pick a random pitch to be used by the player for duration of this session
|
||||
samplePitch = 0.75f + RNG.NextDouble(0f, 0.75f);
|
||||
}
|
||||
|
||||
public PlayerPanelDisplayMode DisplayMode
|
||||
@@ -481,6 +506,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
|
||||
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
|
||||
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
|
||||
|
||||
// only play jump sample if panel is visible
|
||||
if (Alpha > 0)
|
||||
playJumpSample(isConsecutive);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -498,6 +528,44 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
downloadProgressBar.ResizeWidthTo(availability.DownloadProgress ?? 0, 200, Easing.OutPow10);
|
||||
});
|
||||
|
||||
private void playJumpSample(bool rejumping)
|
||||
{
|
||||
bool isLocalUser = User.OnlineID == client.LocalUser?.UserID;
|
||||
|
||||
if (isLocalUser)
|
||||
playInteractionSample(rejumping ? InteractionSampleType.PlayerReJump : InteractionSampleType.PlayerJump);
|
||||
else
|
||||
playInteractionSample(InteractionSampleType.OtherPlayerJump);
|
||||
}
|
||||
|
||||
private void playInteractionSample(InteractionSampleType sampleType)
|
||||
{
|
||||
bool enoughTimePassedSinceLastPlayback = lastSamplePlayback == null || Time.Current - lastSamplePlayback.Value >= OsuGameBase.SAMPLE_DEBOUNCE_TIME;
|
||||
if (!enoughTimePassedSinceLastPlayback)
|
||||
return;
|
||||
|
||||
Sample? targetSample = interactionSamples[sampleType];
|
||||
SampleChannel? targetChannel = interactionSampleChannels.GetValueOrDefault(sampleType);
|
||||
|
||||
targetChannel?.Stop();
|
||||
targetChannel = targetSample?.GetChannel();
|
||||
|
||||
if (targetChannel == null)
|
||||
return;
|
||||
|
||||
float horizontalPos = BoundingBox.Centre.X / Parent!.ToLocalSpace(Parent!.ScreenSpaceDrawQuad).Width;
|
||||
// rescale balance from 0..1 to -1..1
|
||||
float balance = -1f + horizontalPos * 2f;
|
||||
|
||||
targetChannel.Frequency.Value = samplePitch;
|
||||
targetChannel.Balance.Value = balance * OsuGameBase.SFX_STEREO_STRENGTH;
|
||||
targetChannel.Play();
|
||||
|
||||
interactionSampleChannels[sampleType] = targetChannel;
|
||||
|
||||
lastSamplePlayback = Time.Current;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// 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.Sprites;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Screens.Footer;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
public partial class ScreenMatchmaking
|
||||
{
|
||||
private partial class HistoryFooterButton : ScreenFooterButton
|
||||
{
|
||||
[Resolved]
|
||||
private OsuGame? game { get; set; }
|
||||
|
||||
private readonly MultiplayerRoom room;
|
||||
|
||||
public HistoryFooterButton(MultiplayerRoom room)
|
||||
{
|
||||
this.room = room;
|
||||
|
||||
Action = openRoomHistory;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Text = "History";
|
||||
Icon = FontAwesome.Solid.Globe;
|
||||
AccentColour = colours.Lime1;
|
||||
}
|
||||
|
||||
private void openRoomHistory()
|
||||
=> game?.OpenUrlExternally($@"/multiplayer/rooms/{room.RoomID}/events");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -29,6 +30,7 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Gameplay;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Users;
|
||||
@@ -47,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
/// </summary>
|
||||
private const float row_padding = 10;
|
||||
|
||||
private static readonly Vector2 chat_size = new Vector2(550, 130);
|
||||
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
@@ -104,8 +108,12 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Size = new Vector2(700, 130),
|
||||
Margin = new MarginPadding { Bottom = 10, Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING },
|
||||
Size = chat_size,
|
||||
Margin = new MarginPadding
|
||||
{
|
||||
Right = WaveOverlayContainer.WIDTH_PADDING - HORIZONTAL_OVERFLOW_PADDING,
|
||||
Bottom = row_padding
|
||||
},
|
||||
Alpha = 0
|
||||
};
|
||||
}
|
||||
@@ -162,9 +170,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
[
|
||||
new Container
|
||||
{
|
||||
Name = "Chat Area Space",
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Size = new Vector2(700, 130),
|
||||
Size = new Vector2(550, 130),
|
||||
Margin = new MarginPadding { Bottom = row_padding }
|
||||
}
|
||||
]
|
||||
@@ -326,6 +335,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
return false;
|
||||
}
|
||||
|
||||
public override IReadOnlyList<ScreenFooterButton> CreateFooterButtons() =>
|
||||
[
|
||||
new HistoryFooterButton(room)
|
||||
];
|
||||
|
||||
public override void OnEntering(ScreenTransitionEvent e)
|
||||
{
|
||||
base.OnEntering(e);
|
||||
@@ -463,7 +477,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
|
||||
|
||||
// This component is added to the screen footer which is only about 50px high.
|
||||
// Therefore, it's given a large absolute size to give the context menu enough space to display correctly.
|
||||
Size = new Vector2(700);
|
||||
Size = new Vector2(chat_size.X);
|
||||
|
||||
InternalChild = new OsuContextMenuContainer
|
||||
{
|
||||
|
||||
@@ -165,7 +165,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private void updateDisplay(ValueChangedEvent<Period?> period)
|
||||
{
|
||||
FinishTransforms(true);
|
||||
Scheduler.CancelDelayedTasks();
|
||||
|
||||
if (period.NewValue == null)
|
||||
@@ -180,12 +179,12 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
remainingTimeAdjustmentBox
|
||||
.ResizeWidthTo(remaining_time_container_max_size, BREAK_FADE_DURATION, Easing.OutQuint)
|
||||
.Delay(b.Duration - BREAK_FADE_DURATION)
|
||||
.Delay(b.Duration)
|
||||
.ResizeWidthTo(0);
|
||||
|
||||
remainingTimeBox.ResizeWidthTo(remainingTimeForCurrentPeriod);
|
||||
|
||||
remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration);
|
||||
remainingTimeCounter.CountTo(b.Duration + BREAK_FADE_DURATION).CountTo(0, b.Duration + BREAK_FADE_DURATION);
|
||||
|
||||
remainingTimeCounter.MoveToX(-50)
|
||||
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
|
||||
@@ -193,7 +192,7 @@ namespace osu.Game.Screens.Play
|
||||
info.MoveToX(50)
|
||||
.MoveToX(0, BREAK_FADE_DURATION, Easing.OutQuint);
|
||||
|
||||
using (BeginDelayedSequence(b.Duration - BREAK_FADE_DURATION))
|
||||
using (BeginDelayedSequence(b.Duration))
|
||||
{
|
||||
fadeContainer.FadeOut(BREAK_FADE_DURATION);
|
||||
breakArrows.Hide(BREAK_FADE_DURATION);
|
||||
|
||||
@@ -23,6 +23,7 @@ using osu.Game.Input.Bindings;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Utils;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
@@ -256,7 +257,7 @@ namespace osu.Game.Screens.Play
|
||||
if (gameplayState != null)
|
||||
{
|
||||
playInfoText.NewLine();
|
||||
playInfoText.AddText(SongSelectStrings.Accuracy);
|
||||
playInfoText.AddText(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy);
|
||||
playInfoText.AddText(": ");
|
||||
playInfoText.AddText(gameplayState!.ScoreProcessor.Accuracy.Value.FormatAccuracy(), cp => cp.Font = cp.Font.With(weight: FontWeight.Bold));
|
||||
}
|
||||
|
||||
@@ -61,8 +61,6 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
private void updateDisplay(ValueChangedEvent<Period?> period)
|
||||
{
|
||||
FinishTransforms(true);
|
||||
|
||||
if (period.NewValue == null)
|
||||
return;
|
||||
|
||||
@@ -71,7 +69,7 @@ namespace osu.Game.Screens.Play
|
||||
using (BeginAbsoluteSequence(b.Start))
|
||||
{
|
||||
fadeContainer.FadeInFromZero(BreakOverlay.BREAK_FADE_DURATION);
|
||||
using (BeginDelayedSequence(b.Duration - BreakOverlay.BREAK_FADE_DURATION))
|
||||
using (BeginDelayedSequence(b.Duration))
|
||||
fadeContainer.FadeOut(BreakOverlay.BREAK_FADE_DURATION);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
[Cached(typeof(IPreviewTrackOwner))]
|
||||
public partial class SoloSpectatorScreen : SpectatorScreen, IPreviewTrackOwner
|
||||
{
|
||||
[Resolved]
|
||||
|
||||
@@ -328,7 +328,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
liveCollection.PerformWrite(c =>
|
||||
Task.Run(() => liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
@@ -346,7 +346,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
|
||||
@@ -459,6 +459,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
// - Background user tag population runs and causes a realm update.
|
||||
// We don't display user tags so want to ignore this.
|
||||
bool equalForDisplayPurposes =
|
||||
// covers import-as-update flows, such as updating the beatmap with the latest online versions, or external editing inside editor
|
||||
oldBeatmap.ID == newBeatmap.ID &&
|
||||
// covers metadata changes
|
||||
oldBeatmap.Hash == newBeatmap.Hash &&
|
||||
// sanity check
|
||||
@@ -483,6 +485,15 @@ namespace osu.Game.Screens.SelectV2
|
||||
}
|
||||
}
|
||||
|
||||
protected override void FindCarouselItemsForSelection(ref Selection keyboardSelection, ref Selection selection, IList<CarouselItem> items)
|
||||
{
|
||||
if (keyboardSelection.Model != null && grouping.ItemMap.TryGetValue(keyboardSelection.Model, out var keyboardSelectionItem))
|
||||
keyboardSelection = keyboardSelection with { CarouselItem = keyboardSelectionItem.item, Index = keyboardSelectionItem.index };
|
||||
|
||||
if (selection.Model != null && grouping.ItemMap.TryGetValue(selection.Model, out var selectionItem))
|
||||
selection = selection with { CarouselItem = selectionItem.item, Index = selectionItem.index };
|
||||
}
|
||||
|
||||
protected override void HandleFilterCompleted()
|
||||
{
|
||||
base.HandleFilterCompleted();
|
||||
@@ -497,14 +508,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
// The filter might have changed the set of available groups, which means that the current selection may point to a stale group.
|
||||
// Check whether that is the case.
|
||||
bool groupingRemainsOff = currentGroupedBeatmap?.Group == null && grouping.GroupItems.Count == 0;
|
||||
|
||||
bool groupStillValid = false;
|
||||
|
||||
if (currentGroupedBeatmap?.Group != null)
|
||||
{
|
||||
groupStillValid = grouping.GroupItems.TryGetValue(currentGroupedBeatmap.Group, out var items)
|
||||
&& items.Any(i => CheckModelEquality(i.Model, currentGroupedBeatmap));
|
||||
}
|
||||
bool groupStillValid = currentGroupedBeatmap?.Group != null && grouping.ItemMap.ContainsKey(currentGroupedBeatmap);
|
||||
|
||||
if (groupingRemainsOff || groupStillValid)
|
||||
{
|
||||
@@ -697,9 +701,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (CheckModelEquality(ExpandedGroup, CurrentGroupedBeatmap.Group))
|
||||
return;
|
||||
|
||||
var groupItem = GetCarouselItems()?.FirstOrDefault(i => CheckModelEquality(i.Model, CurrentGroupedBeatmap.Group));
|
||||
if (groupItem != null)
|
||||
Activate(groupItem);
|
||||
if (grouping.ItemMap.TryGetValue(CurrentGroupedBeatmap.Group, out var groupItem))
|
||||
Activate(groupItem.item);
|
||||
}
|
||||
|
||||
protected override double? GetScrollTarget()
|
||||
@@ -710,9 +713,13 @@ namespace osu.Game.Screens.SelectV2
|
||||
// attempt a fallback to other possibly expanded panels (set first, then group)
|
||||
if (target == null)
|
||||
{
|
||||
var items = GetCarouselItems();
|
||||
var targetItem = items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedBeatmapSet))
|
||||
?? items?.FirstOrDefault(i => CheckModelEquality(i.Model, ExpandedGroup));
|
||||
CarouselItem? targetItem = null;
|
||||
|
||||
if (ExpandedBeatmapSet != null && grouping.ItemMap.TryGetValue(ExpandedBeatmapSet, out var setItem))
|
||||
targetItem = setItem.item;
|
||||
|
||||
if (targetItem == null && ExpandedGroup != null && grouping.ItemMap.TryGetValue(ExpandedGroup, out var groupItem))
|
||||
targetItem = groupItem.item;
|
||||
|
||||
target = targetItem?.CarouselYPosition;
|
||||
}
|
||||
@@ -922,9 +929,6 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (x is BeatmapInfo beatmapInfoX && y is BeatmapInfo beatmapInfoY)
|
||||
return beatmapInfoX.Equals(beatmapInfoY);
|
||||
|
||||
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
|
||||
return groupX.Equals(groupY);
|
||||
|
||||
if (x is StarDifficultyGroupDefinition starX && y is StarDifficultyGroupDefinition starY)
|
||||
return starX.Equals(starY);
|
||||
|
||||
@@ -934,6 +938,14 @@ namespace osu.Game.Screens.SelectV2
|
||||
if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY)
|
||||
return statusX.Equals(statusY);
|
||||
|
||||
// NOTE: this branch must be AFTER all branches that compare `GroupDefinition` subtypes!
|
||||
// this is an optimisation measure. any subclass of `GroupDefinition` will pass the `is GroupDefinition` check,
|
||||
// and testing a subclass of `GroupDefinition` against any other `GroupDefinition` (or subclass thereof)
|
||||
// will result in a casting cascade of `Equals(GroupDefinition) -> Equals(object) -> Equals(GroupDefinitionSubClass)`
|
||||
// (that last one only if the type check passes)
|
||||
if (x is GroupDefinition groupX && y is GroupDefinition groupY)
|
||||
return groupX.Equals(groupY);
|
||||
|
||||
return base.CheckModelEquality(x, y);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public int BeatmapItemsCount { get; private set; }
|
||||
|
||||
public IDictionary<object, (CarouselItem item, int index)> ItemMap => itemMap;
|
||||
|
||||
/// <summary>
|
||||
/// Beatmap sets contain difficulties as related panels. This dictionary holds the relationships between set-difficulties to allow expanding them on selection.
|
||||
/// </summary>
|
||||
@@ -36,6 +38,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
/// </summary>
|
||||
public IDictionary<GroupDefinition, HashSet<CarouselItem>> GroupItems => groupMap;
|
||||
|
||||
private Dictionary<object, (CarouselItem, int)> itemMap = new Dictionary<object, (CarouselItem, int)>();
|
||||
private Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>> setMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>();
|
||||
private Dictionary<GroupDefinition, HashSet<CarouselItem>> groupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>();
|
||||
|
||||
@@ -49,6 +52,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
// preallocate space for the new mappings using last known estimates
|
||||
var newItemMap = new Dictionary<object, (CarouselItem, int)>(itemMap.Count);
|
||||
var newSetMap = new Dictionary<GroupedBeatmapSet, HashSet<CarouselItem>>(setMap.Count);
|
||||
var newGroupMap = new Dictionary<GroupDefinition, HashSet<CarouselItem>>(groupMap.Count);
|
||||
|
||||
@@ -127,6 +131,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
newItems.Add(i);
|
||||
|
||||
newItemMap[i.Model] = (i, newItems.Count - 1);
|
||||
currentGroupItems?.Add(i);
|
||||
currentSetItems?.Add(i);
|
||||
|
||||
@@ -136,6 +141,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Interlocked.Exchange(ref itemMap, newItemMap);
|
||||
Interlocked.Exchange(ref setMap, newSetMap);
|
||||
Interlocked.Exchange(ref groupMap, newGroupMap);
|
||||
BeatmapItemsCount = displayedBeatmapsCount;
|
||||
@@ -209,7 +215,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
case GroupMode.Collections:
|
||||
{
|
||||
var collections = GetCollections();
|
||||
return getGroupsBy(b => defineGroupByCollection(b, collections), items);
|
||||
return defineGroupsByCollection(items, collections);
|
||||
}
|
||||
|
||||
case GroupMode.MyMaps:
|
||||
@@ -396,29 +402,56 @@ namespace osu.Game.Screens.SelectV2
|
||||
return new GroupDefinition(0, source).Yield();
|
||||
}
|
||||
|
||||
private IEnumerable<GroupDefinition> defineGroupByCollection(BeatmapInfo beatmap, List<BeatmapCollection> collections)
|
||||
private List<GroupMapping> defineGroupsByCollection(List<CarouselItem> carouselItems, List<BeatmapCollection> allCollections)
|
||||
{
|
||||
bool anyCollections = false;
|
||||
Dictionary<GroupDefinition, GroupMapping> groupMappings = new Dictionary<GroupDefinition, GroupMapping>();
|
||||
// this is a pre-built mapping of MD5s to a list of collections in which this MD5 is found in.
|
||||
// the reason to pre-build this is that `BeatmapCollection.BeatmapMD5Hashes` is a list and therefore a naive implementation would be slow,
|
||||
// particularly in edge cases where most beatmaps are in more than one collection.
|
||||
Dictionary<string, List<GroupDefinition>> md5ToCollectionsMap = new Dictionary<string, List<GroupDefinition>>();
|
||||
|
||||
for (int i = 0; i < collections.Count; i++)
|
||||
for (int i = 0; i < allCollections.Count; i++)
|
||||
{
|
||||
var collection = collections[i];
|
||||
var collection = allCollections[i];
|
||||
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved.
|
||||
// the fallback to ordering by name cannot be relied on.
|
||||
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
|
||||
var groupDefinition = new GroupDefinition(i, collection.Name);
|
||||
groupMappings[groupDefinition] = new GroupMapping(groupDefinition, []);
|
||||
|
||||
if (collection.BeatmapMD5Hashes.Contains(beatmap.MD5Hash))
|
||||
foreach (string md5 in collection.BeatmapMD5Hashes)
|
||||
{
|
||||
// NOTE: the ordering of the incoming collection list is significant and needs to be preserved.
|
||||
// the fallback to ordering by name cannot be relied on.
|
||||
// see xmldoc of `BeatmapCarousel.GetAllCollections()`.
|
||||
yield return new GroupDefinition(i, collection.Name);
|
||||
if (!md5ToCollectionsMap.TryGetValue(md5, out var collections))
|
||||
md5ToCollectionsMap[md5] = collections = new List<GroupDefinition>();
|
||||
|
||||
anyCollections = true;
|
||||
collections.Add(groupDefinition);
|
||||
}
|
||||
}
|
||||
|
||||
if (anyCollections)
|
||||
yield break;
|
||||
var notInCollection = new GroupDefinition(int.MaxValue, "Not in collection");
|
||||
groupMappings[notInCollection] = new GroupMapping(notInCollection, []);
|
||||
|
||||
yield return new GroupDefinition(int.MaxValue, "Not in collection");
|
||||
foreach (var item in carouselItems)
|
||||
{
|
||||
var beatmap = (BeatmapInfo)item.Model;
|
||||
|
||||
// as a side note, even reading the `MD5Hash` off a realm model is slow if done enough times,
|
||||
// so it definitely helps that thanks to the mapping it needs to only be retrieved once
|
||||
if (md5ToCollectionsMap.TryGetValue(beatmap.MD5Hash, out var collections))
|
||||
{
|
||||
foreach (var collection in collections)
|
||||
groupMappings[collection].ItemsInGroup.Add(item);
|
||||
}
|
||||
else
|
||||
groupMappings[notInCollection].ItemsInGroup.Add(item);
|
||||
}
|
||||
|
||||
return groupMappings.Values
|
||||
// safety against potentially empty eagerly-initialised groups
|
||||
// (could happen if user has a collection with MD5s of maps that aren't locally available)
|
||||
.Where(mapping => mapping.ItemsInGroup.Count > 0)
|
||||
.OrderBy(mapping => mapping.Group!.Order)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private IEnumerable<GroupDefinition> defineGroupByOwnMaps(BeatmapInfo beatmap, int? localUserId, string? localUserUsername)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
@@ -237,11 +238,11 @@ namespace osu.Game.Screens.SelectV2
|
||||
{
|
||||
Debug.Assert(collection != null);
|
||||
|
||||
collection.PerformWrite(c =>
|
||||
Task.Run(() => collection.PerformWrite(c =>
|
||||
{
|
||||
if (!c.BeatmapMD5Hashes.Remove(beatmap.Value.BeatmapInfo.MD5Hash))
|
||||
c.BeatmapMD5Hashes.Add(beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
protected override Drawable CreateContent() => (Content)base.CreateContent();
|
||||
|
||||
@@ -5,6 +5,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
@@ -297,7 +298,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
|
||||
return new TernaryStateToggleMenuItem(collection.Name, MenuItemType.Standard, s =>
|
||||
{
|
||||
liveCollection.PerformWrite(c =>
|
||||
Task.Run(() => liveCollection.PerformWrite(c =>
|
||||
{
|
||||
foreach (var b in beatmapSet.Beatmaps)
|
||||
{
|
||||
@@ -315,7 +316,7 @@ namespace osu.Game.Screens.SelectV2
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}));
|
||||
})
|
||||
{
|
||||
State = { Value = state }
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.IO;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Skinning
|
||||
{
|
||||
@@ -40,6 +41,23 @@ namespace osu.Game.Skinning
|
||||
new NamespacedResourceStore<byte[]>(resources.Resources, "Skins/Retro")
|
||||
)
|
||||
{
|
||||
Configuration.ConfigDictionary[@"SliderBallFlip"] = "0";
|
||||
Configuration.ConfigDictionary[@"SliderBallFrames"] = "10";
|
||||
Configuration.ConfigDictionary[@"AllowSliderBallTint"] = "0";
|
||||
Configuration.ConfigDictionary[@"CursorTrailRotate"] = "0";
|
||||
Configuration.ConfigDictionary[@"Version"] = "1";
|
||||
|
||||
Configuration.CustomComboColours =
|
||||
[
|
||||
new Color4(255, 150, 0, 255),
|
||||
new Color4(5, 240, 5, 255),
|
||||
new Color4(5, 5, 240, 255),
|
||||
new Color4(240, 5, 5, 255)
|
||||
];
|
||||
|
||||
Configuration.ConfigDictionary[@"HitCircleOverlap"] = "3";
|
||||
Configuration.ConfigDictionary[@"ScoreOverlap"] = "3";
|
||||
Configuration.ConfigDictionary[@"ComboOverlap"] = "3";
|
||||
}
|
||||
|
||||
public override Texture? GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
|
||||
|
||||
@@ -228,8 +228,9 @@ namespace osu.Game.Skinning
|
||||
// First attempt to deserialise using the new SkinLayoutInfo format
|
||||
layout = JsonConvert.DeserializeObject<SkinLayoutInfo>(jsonContent);
|
||||
}
|
||||
catch
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($"Deserialising skin layout to {nameof(SkinLayoutInfo)} failed. Falling back to {nameof(SerialisedDrawableInfo)}[].\nDetails: {ex}");
|
||||
}
|
||||
|
||||
// If deserialisation using SkinLayoutInfo fails, attempt to deserialise using the old naked list.
|
||||
|
||||
@@ -178,9 +178,10 @@ namespace osu.Game.Skinning
|
||||
|
||||
if (existingFile == null)
|
||||
{
|
||||
// skins without a skin.ini are supposed to import using the "latest version" spec.
|
||||
// skins without a skin.ini are supposed to import using the "latest version" spec, unless we're making a copy of the retro skin which specifies 1.0.
|
||||
// see https://github.com/peppy/osu-stable-reference/blob/1531237b63392e82c003c712faa028406073aa8f/osu!/Graphics/Skinning/SkinManager.cs#L297-L298
|
||||
newLines.Add(FormattableString.Invariant($"Version: {SkinConfiguration.LATEST_VERSION}"));
|
||||
decimal version = item.InstantiationInfo == typeof(RetroSkin).GetInvariantInstantiationInfo() ? 1.0M : SkinConfiguration.LATEST_VERSION;
|
||||
newLines.Add(FormattableString.Invariant($"Version: {version}"));
|
||||
|
||||
// In the case a skin doesn't have a skin.ini yet, let's create one.
|
||||
writeNewSkinIni();
|
||||
|
||||
@@ -86,9 +86,7 @@ namespace osu.Game.Tests.Beatmaps
|
||||
currentTestBeatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
|
||||
|
||||
// populate ruleset for beatmap converters that require it to be present.
|
||||
var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID);
|
||||
|
||||
Debug.Assert(ruleset != null);
|
||||
var ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID) ?? new RulesetInfo { OnlineID = currentTestBeatmap.BeatmapInfo.Ruleset.OnlineID };
|
||||
|
||||
currentTestBeatmap.BeatmapInfo.Ruleset = ruleset;
|
||||
});
|
||||
|
||||
@@ -35,9 +35,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="20.1.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.1028.0" />
|
||||
<PackageReference Include="jvnkosu.Resources" Version="2025.1103.0" />
|
||||
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2025.1118.1" />
|
||||
<PackageReference Include="jvnkosu.Resources" Version="2025.1119.0" />
|
||||
<PackageReference Include="Sentry" Version="5.1.1" />
|
||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||
|
||||
@@ -17,6 +17,6 @@
|
||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1028.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.1118.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user