mirror of
https://github.com/ppy/osu.git
synced 2025-01-18 11:02:57 +08:00
Merge branch 'master' into mobile-fix-mania
This commit is contained in:
commit
42e5cb58b7
@ -37,7 +37,7 @@ You can also generally download a version for your current device from the [osu!
|
|||||||
|
|
||||||
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
If your platform is unsupported or not listed above, there is still a chance you can run the release or manually build it by following the instructions below.
|
||||||
|
|
||||||
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon to se don't have to live with this limitation.
|
**For iOS/iPadOS users**: The iOS testflight link fills up very fast (Apple has a hard limit of 10,000 users). We reset it occasionally. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements. Our goal is to get the game on mobile app stores very soon so we don't have to live with this limitation.
|
||||||
|
|
||||||
## Developing a custom ruleset
|
## Developing a custom ruleset
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1224.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.114.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||||
|
@ -13,7 +13,6 @@ using Android.Graphics;
|
|||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using osu.Framework.Android;
|
using osu.Framework.Android;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using Debug = System.Diagnostics.Debug;
|
using Debug = System.Diagnostics.Debug;
|
||||||
using Uri = Android.Net.Uri;
|
using Uri = Android.Net.Uri;
|
||||||
@ -52,9 +51,23 @@ namespace osu.Android
|
|||||||
|
|
||||||
public bool IsTablet { get; private set; }
|
public bool IsTablet { get; private set; }
|
||||||
|
|
||||||
private OsuGameAndroid game = null!;
|
private readonly OsuGameAndroid game;
|
||||||
|
|
||||||
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
private bool gameCreated;
|
||||||
|
|
||||||
|
protected override Framework.Game CreateGame()
|
||||||
|
{
|
||||||
|
if (gameCreated)
|
||||||
|
throw new InvalidOperationException("Framework tried to create a game twice.");
|
||||||
|
|
||||||
|
gameCreated = true;
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
public OsuGameActivity()
|
||||||
|
{
|
||||||
|
game = new OsuGameAndroid(this);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnCreate(Bundle? savedInstanceState)
|
protected override void OnCreate(Bundle? savedInstanceState)
|
||||||
{
|
{
|
||||||
@ -97,25 +110,38 @@ namespace osu.Android
|
|||||||
|
|
||||||
private void handleIntent(Intent? intent)
|
private void handleIntent(Intent? intent)
|
||||||
{
|
{
|
||||||
switch (intent?.Action)
|
if (intent == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (intent.Action)
|
||||||
{
|
{
|
||||||
case Intent.ActionDefault:
|
case Intent.ActionDefault:
|
||||||
if (intent.Scheme == ContentResolver.SchemeContent)
|
if (intent.Scheme == ContentResolver.SchemeContent)
|
||||||
handleImportFromUris(intent.Data.AsNonNull());
|
{
|
||||||
|
if (intent.Data != null)
|
||||||
|
handleImportFromUris(intent.Data);
|
||||||
|
}
|
||||||
else if (osu_url_schemes.Contains(intent.Scheme))
|
else if (osu_url_schemes.Contains(intent.Scheme))
|
||||||
|
{
|
||||||
|
if (intent.DataString != null)
|
||||||
game.HandleLink(intent.DataString);
|
game.HandleLink(intent.DataString);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Intent.ActionSend:
|
case Intent.ActionSend:
|
||||||
case Intent.ActionSendMultiple:
|
case Intent.ActionSendMultiple:
|
||||||
{
|
{
|
||||||
|
if (intent.ClipData == null)
|
||||||
|
break;
|
||||||
|
|
||||||
var uris = new List<Uri>();
|
var uris = new List<Uri>();
|
||||||
|
|
||||||
for (int i = 0; i < intent.ClipData?.ItemCount; i++)
|
for (int i = 0; i < intent.ClipData.ItemCount; i++)
|
||||||
{
|
{
|
||||||
var content = intent.ClipData?.GetItemAt(i);
|
var item = intent.ClipData.GetItemAt(i);
|
||||||
if (content != null)
|
if (item?.Uri != null)
|
||||||
uris.Add(content.Uri.AsNonNull());
|
uris.Add(item.Uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleImportFromUris(uris.ToArray());
|
handleImportFromUris(uris.ToArray());
|
||||||
|
@ -18,7 +18,6 @@ using osu.Game.Rulesets.Edit.Tools;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Screens.Edit.Components.TernaryButtons;
|
|
||||||
using osu.Game.Screens.Edit.Compose.Components;
|
using osu.Game.Screens.Edit.Compose.Components;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -72,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
protected override Drawable CreateHitObjectInspector() => new CatchHitObjectInspector(DistanceSnapProvider);
|
||||||
|
|
||||||
protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
.Concat(DistanceSnapProvider.CreateTernaryButtons());
|
||||||
|
|
||||||
|
@ -159,27 +159,26 @@ namespace osu.Game.Rulesets.Catch.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu! ruleset's implementation.
|
// Note that this implementation is shared with the osu! ruleset's implementation.
|
||||||
// If a change is made here, OsuHitObject.cs should also be updated.
|
// If a change is made here, OsuHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (this is BananaShower)
|
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||||
{
|
// - At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
||||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a banana shower are both enforced to be a new combo,
|
|
||||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||||
if (NewCombo || lastObj == null || lastObj is BananaShower)
|
if (this is not BananaShower && (NewCombo || lastObj == null || lastObj is BananaShower))
|
||||||
{
|
{
|
||||||
IndexInCurrentCombo = 0;
|
inCurrentCombo = 0;
|
||||||
ComboIndex++;
|
index++;
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Audio.Sample;
|
using osu.Framework.Audio.Sample;
|
||||||
@ -17,6 +18,7 @@ using osu.Framework.Graphics.Textures;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Testing.Input;
|
using osu.Framework.Testing.Input;
|
||||||
using osu.Game.Audio;
|
using osu.Game.Audio;
|
||||||
|
using osu.Game.Rulesets.Osu.Skinning;
|
||||||
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
using osu.Game.Rulesets.Osu.Skinning.Legacy;
|
||||||
using osu.Game.Rulesets.Osu.UI.Cursor;
|
using osu.Game.Rulesets.Osu.UI.Cursor;
|
||||||
using osu.Game.Skinning;
|
using osu.Game.Skinning;
|
||||||
@ -103,6 +105,23 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
AddStep("contract", () => this.ChildrenOfType<CursorTrail>().Single().NewPartScale = Vector2.One);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRotation()
|
||||||
|
{
|
||||||
|
createTest(() =>
|
||||||
|
{
|
||||||
|
var skinContainer = new LegacySkinContainer(renderer, provideMiddle: true, enableRotation: true);
|
||||||
|
var legacyCursorTrail = new LegacyRotatingCursorTrail(skinContainer)
|
||||||
|
{
|
||||||
|
NewPartScale = new Vector2(10)
|
||||||
|
};
|
||||||
|
|
||||||
|
skinContainer.Child = legacyCursorTrail;
|
||||||
|
|
||||||
|
return skinContainer;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
private void createTest(Func<Drawable> createContent) => AddStep("create trail", () =>
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
@ -121,12 +140,14 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
private readonly IRenderer renderer;
|
private readonly IRenderer renderer;
|
||||||
private readonly bool provideMiddle;
|
private readonly bool provideMiddle;
|
||||||
private readonly bool provideCursor;
|
private readonly bool provideCursor;
|
||||||
|
private readonly bool enableRotation;
|
||||||
|
|
||||||
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true)
|
public LegacySkinContainer(IRenderer renderer, bool provideMiddle, bool provideCursor = true, bool enableRotation = false)
|
||||||
{
|
{
|
||||||
this.renderer = renderer;
|
this.renderer = renderer;
|
||||||
this.provideMiddle = provideMiddle;
|
this.provideMiddle = provideMiddle;
|
||||||
this.provideCursor = provideCursor;
|
this.provideCursor = provideCursor;
|
||||||
|
this.enableRotation = enableRotation;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
}
|
}
|
||||||
@ -152,7 +173,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
|
|
||||||
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
public ISample GetSample(ISampleInfo sampleInfo) => null;
|
||||||
|
|
||||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => null;
|
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
|
||||||
|
{
|
||||||
|
switch (lookup)
|
||||||
|
{
|
||||||
|
case OsuSkinConfiguration osuLookup:
|
||||||
|
if (osuLookup == OsuSkinConfiguration.CursorTrailRotate)
|
||||||
|
return SkinUtils.As<TValue>(new BindableBool(enableRotation));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||||
|
|
||||||
@ -185,5 +218,19 @@ namespace osu.Game.Rulesets.Osu.Tests
|
|||||||
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
MoveMouseTo(ToScreenSpace(DrawSize / 2 + DrawSize / 3 * rPos));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class LegacyRotatingCursorTrail : LegacyCursorTrail
|
||||||
|
{
|
||||||
|
public LegacyRotatingCursorTrail([NotNull] ISkin skin)
|
||||||
|
: base(skin)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
PartRotation += (float)(Time.Elapsed * 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
|||||||
|
|
||||||
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
protected override Drawable CreateHitObjectInspector() => new OsuHitObjectInspector();
|
||||||
|
|
||||||
protected override IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
|
protected override IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
=> base.CreateTernaryButtons()
|
=> base.CreateTernaryButtons()
|
||||||
.Append(new DrawableTernaryButton
|
.Append(new DrawableTernaryButton
|
||||||
{
|
{
|
||||||
|
@ -184,27 +184,26 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
{
|
{
|
||||||
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
// Note that this implementation is shared with the osu!catch ruleset's implementation.
|
||||||
// If a change is made here, CatchHitObject.cs should also be updated.
|
// If a change is made here, CatchHitObject.cs should also be updated.
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (this is Spinner)
|
// - For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
||||||
{
|
// - At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
||||||
// For the purpose of combo colours, spinners never start a new combo even if they are flagged as doing so.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At decode time, the first hitobject in the beatmap and the first hitobject after a spinner are both enforced to be a new combo,
|
|
||||||
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
// but this isn't directly enforced by the editor so the extra checks against the last hitobject are duplicated here.
|
||||||
if (NewCombo || lastObj == null || lastObj is Spinner)
|
if (this is not Spinner && (NewCombo || lastObj == null || lastObj is Spinner))
|
||||||
{
|
{
|
||||||
IndexInCurrentCombo = 0;
|
inCurrentCombo = 0;
|
||||||
ComboIndex++;
|
index++;
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
protected override HitWindows CreateHitWindows() => new OsuHitWindows();
|
||||||
|
@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
public partial class LegacyCursor : SkinnableCursor
|
public partial class LegacyCursor : SkinnableCursor
|
||||||
{
|
{
|
||||||
|
public static readonly int REVOLUTION_DURATION = 10000;
|
||||||
|
|
||||||
private const float pressed_scale = 1.3f;
|
private const float pressed_scale = 1.3f;
|
||||||
private const float released_scale = 1f;
|
private const float released_scale = 1f;
|
||||||
|
|
||||||
@ -52,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
if (spin)
|
if (spin)
|
||||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
ExpandTarget.Spin(REVOLUTION_DURATION, RotationDirection.Clockwise);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Expand()
|
public override void Expand()
|
||||||
|
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
private void load(OsuConfigManager config, ISkinSource skinSource)
|
private void load(OsuConfigManager config, ISkinSource skinSource)
|
||||||
{
|
{
|
||||||
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
|
||||||
|
AllowPartRotation = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.CursorTrailRotate)?.Value ?? true;
|
||||||
|
|
||||||
Texture = skin.GetTexture("cursortrail");
|
Texture = skin.GetTexture("cursortrail");
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
|
|||||||
CursorCentre,
|
CursorCentre,
|
||||||
CursorExpand,
|
CursorExpand,
|
||||||
CursorRotate,
|
CursorRotate,
|
||||||
|
CursorTrailRotate,
|
||||||
HitCircleOverlayAboveNumber,
|
HitCircleOverlayAboveNumber,
|
||||||
|
|
||||||
// ReSharper disable once IdentifierTypo
|
// ReSharper disable once IdentifierTypo
|
||||||
|
@ -34,19 +34,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual float FadeExponent => 1.7f;
|
protected virtual float FadeExponent => 1.7f;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
|
||||||
private int currentIndex;
|
|
||||||
private IShader shader;
|
|
||||||
private double timeOffset;
|
|
||||||
private float time;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The scale used on creation of a new trail part.
|
/// The scale used on creation of a new trail part.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 NewPartScale = Vector2.One;
|
public Vector2 NewPartScale { get; set; } = Vector2.One;
|
||||||
|
|
||||||
private Anchor trailOrigin = Anchor.Centre;
|
/// <summary>
|
||||||
|
/// The rotation (in degrees) to apply to trail parts when <see cref="AllowPartRotation"/> is <c>true</c>.
|
||||||
|
/// </summary>
|
||||||
|
public float PartRotation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to rotate trail parts based on the value of <see cref="PartRotation"/>.
|
||||||
|
/// </summary>
|
||||||
|
protected bool AllowPartRotation { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The trail part texture origin.
|
||||||
|
/// </summary>
|
||||||
protected Anchor TrailOrigin
|
protected Anchor TrailOrigin
|
||||||
{
|
{
|
||||||
get => trailOrigin;
|
get => trailOrigin;
|
||||||
@ -57,6 +62,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
|
private Anchor trailOrigin = Anchor.Centre;
|
||||||
|
private int currentIndex;
|
||||||
|
private IShader shader;
|
||||||
|
private double timeOffset;
|
||||||
|
private float time;
|
||||||
|
|
||||||
public CursorTrail()
|
public CursorTrail()
|
||||||
{
|
{
|
||||||
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
// as we are currently very dependent on having a running clock, let's make our own clock for the time being.
|
||||||
@ -220,6 +232,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
private float time;
|
private float time;
|
||||||
private float fadeExponent;
|
private float fadeExponent;
|
||||||
|
private float angle;
|
||||||
|
|
||||||
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
private readonly TrailPart[] parts = new TrailPart[max_sprites];
|
||||||
private Vector2 originPosition;
|
private Vector2 originPosition;
|
||||||
@ -239,6 +252,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
texture = Source.texture;
|
texture = Source.texture;
|
||||||
time = Source.time;
|
time = Source.time;
|
||||||
fadeExponent = Source.FadeExponent;
|
fadeExponent = Source.FadeExponent;
|
||||||
|
angle = Source.AllowPartRotation ? float.DegreesToRadians(Source.PartRotation) : 0;
|
||||||
|
|
||||||
originPosition = Vector2.Zero;
|
originPosition = Vector2.Zero;
|
||||||
|
|
||||||
@ -279,6 +293,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
renderer.PushLocalMatrix(DrawInfo.Matrix);
|
||||||
|
|
||||||
|
float sin = MathF.Sin(angle);
|
||||||
|
float cos = MathF.Cos(angle);
|
||||||
|
|
||||||
foreach (var part in parts)
|
foreach (var part in parts)
|
||||||
{
|
{
|
||||||
if (part.InvalidationID == -1)
|
if (part.InvalidationID == -1)
|
||||||
@ -289,7 +306,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.BottomLeft,
|
TexturePosition = textureRect.BottomLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||||
@ -298,7 +317,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X,
|
||||||
|
part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y) * part.Scale.Y), part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.BottomRight,
|
TexturePosition = textureRect.BottomRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||||
@ -307,7 +328,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X) * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.TopRight,
|
TexturePosition = textureRect.TopRight,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||||
@ -316,7 +339,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
|
|
||||||
vertexBatch.Add(new TexturedTrailVertex
|
vertexBatch.Add(new TexturedTrailVertex
|
||||||
{
|
{
|
||||||
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
Position = rotateAround(
|
||||||
|
new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X * part.Scale.X, part.Position.Y - texture.DisplayHeight * originPosition.Y * part.Scale.Y),
|
||||||
|
part.Position, sin, cos),
|
||||||
TexturePosition = textureRect.TopLeft,
|
TexturePosition = textureRect.TopLeft,
|
||||||
TextureRect = new Vector4(0, 0, 1, 1),
|
TextureRect = new Vector4(0, 0, 1, 1),
|
||||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||||
@ -330,6 +355,14 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
shader.Unbind();
|
shader.Unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Vector2 rotateAround(Vector2 input, Vector2 origin, float sin, float cos)
|
||||||
|
{
|
||||||
|
float xTranslated = input.X - origin.X;
|
||||||
|
float yTranslated = input.Y - origin.Y;
|
||||||
|
|
||||||
|
return new Vector2(xTranslated * cos - yTranslated * sin, xTranslated * sin + yTranslated * cos) + origin;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
@ -36,6 +36,11 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
public Vector2 CurrentExpandedScale => skinnableCursor.ExpandTarget?.Scale ?? Vector2.One;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current rotation of the cursor.
|
||||||
|
/// </summary>
|
||||||
|
public float CurrentRotation => skinnableCursor.ExpandTarget?.Rotation ?? 0;
|
||||||
|
|
||||||
public IBindable<float> CursorScale => cursorScale;
|
public IBindable<float> CursorScale => cursorScale;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -83,7 +83,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
|||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (cursorTrail.Drawable is CursorTrail trail)
|
if (cursorTrail.Drawable is CursorTrail trail)
|
||||||
|
{
|
||||||
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
trail.NewPartScale = ActiveCursor.CurrentExpandedScale;
|
||||||
|
trail.PartRotation = ActiveCursor.CurrentRotation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -15,6 +16,7 @@ using osu.Framework.Input.Events;
|
|||||||
using osu.Framework.Input.States;
|
using osu.Framework.Input.States;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
@ -31,6 +33,7 @@ using osu.Game.Screens.Play;
|
|||||||
using osu.Game.Screens.Play.PlayerSettings;
|
using osu.Game.Screens.Play.PlayerSettings;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Storyboards.Drawables;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -45,6 +48,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
private LoadBlockingTestPlayer player;
|
private LoadBlockingTestPlayer player;
|
||||||
private BeatmapManager manager;
|
private BeatmapManager manager;
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets;
|
||||||
|
private UpdateCounter storyboardUpdateCounter;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
@ -194,6 +198,28 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStoryboardUpdatesWhenDimmed()
|
||||||
|
{
|
||||||
|
performFullSetup();
|
||||||
|
createFakeStoryboard();
|
||||||
|
|
||||||
|
AddStep("Enable fully dimmed storyboard", () =>
|
||||||
|
{
|
||||||
|
player.StoryboardReplacesBackground.Value = true;
|
||||||
|
player.StoryboardEnabled.Value = true;
|
||||||
|
player.DimmableStoryboard.IgnoreUserSettings.Value = false;
|
||||||
|
songSelect.DimLevel.Value = 1f;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
|
||||||
|
|
||||||
|
AddWaitStep("wait some", 20);
|
||||||
|
|
||||||
|
AddUntilStep("Storyboard is always present", () => player.ChildrenOfType<DrawableStoryboard>().Single().AlwaysPresent, () => Is.True);
|
||||||
|
AddUntilStep("Dimmable storyboard content is being updated", () => storyboardUpdateCounter.StoryboardContentLastUpdated, () => Is.EqualTo(Time.Current).Within(100));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestStoryboardIgnoreUserSettings()
|
public void TestStoryboardIgnoreUserSettings()
|
||||||
{
|
{
|
||||||
@ -269,7 +295,10 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
{
|
{
|
||||||
player.StoryboardEnabled.Value = false;
|
player.StoryboardEnabled.Value = false;
|
||||||
player.StoryboardReplacesBackground.Value = false;
|
player.StoryboardReplacesBackground.Value = false;
|
||||||
player.DimmableStoryboard.Add(new OsuSpriteText
|
player.DimmableStoryboard.AddRange(new Drawable[]
|
||||||
|
{
|
||||||
|
storyboardUpdateCounter = new UpdateCounter(),
|
||||||
|
new OsuSpriteText
|
||||||
{
|
{
|
||||||
Size = new Vector2(500, 50),
|
Size = new Vector2(500, 50),
|
||||||
Alpha = 1,
|
Alpha = 1,
|
||||||
@ -278,6 +307,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Text = "THIS IS A STORYBOARD",
|
Text = "THIS IS A STORYBOARD",
|
||||||
Font = new FontUsage(size: 50)
|
Font = new FontUsage(size: 50)
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -353,7 +383,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Make sure every time a screen gets pushed, the background doesn't get replaced
|
/// Make sure every time a screen gets pushed, the background doesn't get replaced
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Whether or not the original background (The one created in DummySongSelect) is still the current background</returns>
|
/// <returns>Whether the original background (The one created in DummySongSelect) is still the current background</returns>
|
||||||
public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true;
|
public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -384,7 +414,7 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
|
|
||||||
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard;
|
||||||
|
|
||||||
// Whether or not the player should be allowed to load.
|
// Whether the player should be allowed to load.
|
||||||
public bool BlockLoad;
|
public bool BlockLoad;
|
||||||
|
|
||||||
public Bindable<bool> StoryboardEnabled;
|
public Bindable<bool> StoryboardEnabled;
|
||||||
@ -451,6 +481,17 @@ namespace osu.Game.Tests.Visual.Background
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private partial class UpdateCounter : Drawable
|
||||||
|
{
|
||||||
|
public double StoryboardContentLastUpdated;
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
StoryboardContentLastUpdated = Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground
|
private partial class TestDimmableBackground : BackgroundScreenBeatmap.DimmableBackground
|
||||||
{
|
{
|
||||||
public Color4 CurrentColour => Content.Colour;
|
public Color4 CurrentColour => Content.Colour;
|
||||||
|
@ -0,0 +1,129 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Online;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Online.Metadata;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using osu.Game.Tests.Visual.Metadata;
|
||||||
|
using osu.Game.Users;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Components
|
||||||
|
{
|
||||||
|
public partial class TestSceneFriendPresenceNotifier : OsuManualInputManagerTestScene
|
||||||
|
{
|
||||||
|
private ChannelManager channelManager = null!;
|
||||||
|
private NotificationOverlay notificationOverlay = null!;
|
||||||
|
private ChatOverlay chatOverlay = null!;
|
||||||
|
private TestMetadataClient metadataClient = null!;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup() => Schedule(() =>
|
||||||
|
{
|
||||||
|
Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies =
|
||||||
|
[
|
||||||
|
(typeof(ChannelManager), channelManager = new ChannelManager(API)),
|
||||||
|
(typeof(INotificationOverlay), notificationOverlay = new NotificationOverlay()),
|
||||||
|
(typeof(ChatOverlay), chatOverlay = new ChatOverlay()),
|
||||||
|
(typeof(MetadataClient), metadataClient = new TestMetadataClient()),
|
||||||
|
],
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
channelManager,
|
||||||
|
notificationOverlay,
|
||||||
|
chatOverlay,
|
||||||
|
metadataClient,
|
||||||
|
new FriendPresenceNotifier()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 1; i <= 100; i++)
|
||||||
|
((DummyAPIAccess)API).Friends.Add(new APIRelation { TargetID = i, TargetUser = new APIUser { Username = $"Friend {i}" } });
|
||||||
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNotifications()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
AddStep("bring friend 1 offline", () => metadataClient.FriendPresenceUpdated(1, null));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleUserNotificationOpensChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friend 1 online", () => metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("chat overlay opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Visible));
|
||||||
|
AddUntilStep("user channel selected", () => channelManager.CurrentChannel.Value.Name, () => Is.EqualTo(((DummyAPIAccess)API).Friends[0].TargetUser!.Username));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMultipleUserNotificationDoesNotOpenChat()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1 & 2 online", () =>
|
||||||
|
{
|
||||||
|
metadataClient.FriendPresenceUpdated(1, new UserPresence { Status = UserStatus.Online });
|
||||||
|
metadataClient.FriendPresenceUpdated(2, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("click notification", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(this.ChildrenOfType<Notification>().First());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("chat overlay not opened", () => chatOverlay.State.Value, () => Is.EqualTo(Visibility.Hidden));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestNonFriendsDoNotNotify()
|
||||||
|
{
|
||||||
|
AddStep("bring non-friend 1000 online", () => metadataClient.UserPresenceUpdated(1000, new UserPresence { Status = UserStatus.Online }));
|
||||||
|
AddWaitStep("wait for possible notification", 10);
|
||||||
|
AddAssert("no notification", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPostManyDebounced()
|
||||||
|
{
|
||||||
|
AddStep("bring friends 1-10 online", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.FriendPresenceUpdated(i, new UserPresence { Status = UserStatus.Online });
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
|
||||||
|
|
||||||
|
AddStep("bring friends 1-10 offline", () =>
|
||||||
|
{
|
||||||
|
for (int i = 1; i <= 10; i++)
|
||||||
|
metadataClient.FriendPresenceUpdated(i, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notification", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
@ -102,6 +103,77 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMutateProtectedSkinFromMainMenu_UndoToInitialStateIsCorrect()
|
||||||
|
{
|
||||||
|
AddStep("set default skin", () => Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.SetDefault());
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
|
||||||
|
openSkinEditor();
|
||||||
|
AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get<SkinManager>().CurrentSkin.Value.SkinInfo.Value.Protected);
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
return Game.ScreenStack.CurrentScreen is Player;
|
||||||
|
});
|
||||||
|
|
||||||
|
string state = string.Empty;
|
||||||
|
|
||||||
|
AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType<ArgonAccuracyCounter>().Any(counter => counter.Position != new Vector2()));
|
||||||
|
AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()));
|
||||||
|
AddStep("add any component", () => Game.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick());
|
||||||
|
AddStep("undo", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Z);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddUntilStep("only one accuracy meter left",
|
||||||
|
() => Game.ChildrenOfType<Player>().Single().ChildrenOfType<ArgonAccuracyCounter>().Count(),
|
||||||
|
() => Is.EqualTo(1));
|
||||||
|
AddAssert("accuracy meter state unchanged",
|
||||||
|
() => JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()),
|
||||||
|
() => Is.EqualTo(state));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestMutateProtectedSkinFromPlayer_UndoToInitialStateIsCorrect()
|
||||||
|
{
|
||||||
|
AddStep("set default skin", () => Game.Dependencies.Get<SkinManager>().CurrentSkinInfo.SetDefault());
|
||||||
|
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||||
|
advanceToSongSelect();
|
||||||
|
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||||
|
|
||||||
|
AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() });
|
||||||
|
AddStep("enter gameplay", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
|
AddUntilStep("wait for player", () =>
|
||||||
|
{
|
||||||
|
DismissAnyNotifications();
|
||||||
|
return Game.ScreenStack.CurrentScreen is Player;
|
||||||
|
});
|
||||||
|
openSkinEditor();
|
||||||
|
|
||||||
|
string state = string.Empty;
|
||||||
|
|
||||||
|
AddUntilStep("wait for accuracy counter", () => Game.ChildrenOfType<ArgonAccuracyCounter>().Any(counter => counter.Position != new Vector2()));
|
||||||
|
AddStep("dump state of accuracy meter", () => state = JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()));
|
||||||
|
AddStep("add any component", () => Game.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First().TriggerClick());
|
||||||
|
AddStep("undo", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.Key(Key.Z);
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
});
|
||||||
|
AddUntilStep("only one accuracy meter left",
|
||||||
|
() => Game.ChildrenOfType<Player>().Single().ChildrenOfType<ArgonAccuracyCounter>().Count(),
|
||||||
|
() => Is.EqualTo(1));
|
||||||
|
AddAssert("accuracy meter state unchanged",
|
||||||
|
() => JsonConvert.SerializeObject(Game.ChildrenOfType<ArgonAccuracyCounter>().First().CreateSerialisedInfo()),
|
||||||
|
() => Is.EqualTo(state));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestComponentsDeselectedOnSkinEditorHide()
|
public void TestComponentsDeselectedOnSkinEditorHide()
|
||||||
{
|
{
|
||||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
AddUntilStep("section top is visible", () =>
|
AddUntilStep("section top is visible", () =>
|
||||||
{
|
{
|
||||||
var scrollContainer = container.ChildrenOfType<UserTrackingScrollContainer>().Single();
|
var scrollContainer = container.ChildrenOfType<UserTrackingScrollContainer>().Single();
|
||||||
float sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]);
|
double sectionPosition = scrollContainer.GetChildPosInContent(container.Children[scrollIndex]);
|
||||||
return scrollContainer.Current < sectionPosition;
|
return scrollContainer.Current < sectionPosition;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ namespace osu.Game.Configuration
|
|||||||
|
|
||||||
SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
|
SetDefault(OsuSetting.NotifyOnUsernameMentioned, true);
|
||||||
SetDefault(OsuSetting.NotifyOnPrivateMessage, true);
|
SetDefault(OsuSetting.NotifyOnPrivateMessage, true);
|
||||||
|
SetDefault(OsuSetting.NotifyOnFriendPresenceChange, true);
|
||||||
|
|
||||||
// Audio
|
// Audio
|
||||||
SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
|
SetDefault(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01);
|
||||||
@ -418,6 +419,7 @@ namespace osu.Game.Configuration
|
|||||||
IntroSequence,
|
IntroSequence,
|
||||||
NotifyOnUsernameMentioned,
|
NotifyOnUsernameMentioned,
|
||||||
NotifyOnPrivateMessage,
|
NotifyOnPrivateMessage,
|
||||||
|
NotifyOnFriendPresenceChange,
|
||||||
UIHoldActivationDelay,
|
UIHoldActivationDelay,
|
||||||
HitLighting,
|
HitLighting,
|
||||||
StarFountains,
|
StarFountains,
|
||||||
|
@ -59,11 +59,11 @@ namespace osu.Game.Graphics.Containers
|
|||||||
/// <param name="extraScroll">An added amount to scroll beyond the requirement to bring the target into view.</param>
|
/// <param name="extraScroll">An added amount to scroll beyond the requirement to bring the target into view.</param>
|
||||||
public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0)
|
public void ScrollIntoView(Drawable d, bool animated = true, float extraScroll = 0)
|
||||||
{
|
{
|
||||||
float childPos0 = GetChildPosInContent(d);
|
double childPos0 = GetChildPosInContent(d);
|
||||||
float childPos1 = GetChildPosInContent(d, d.DrawSize);
|
double childPos1 = GetChildPosInContent(d, d.DrawSize);
|
||||||
|
|
||||||
float minPos = Math.Min(childPos0, childPos1);
|
double minPos = Math.Min(childPos0, childPos1);
|
||||||
float maxPos = Math.Max(childPos0, childPos1);
|
double maxPos = Math.Max(childPos0, childPos1);
|
||||||
|
|
||||||
if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
|
if (minPos < Current || (minPos > Current && d.DrawSize[ScrollDim] > DisplayableContent))
|
||||||
ScrollTo(minPos - extraScroll, animated);
|
ScrollTo(minPos - extraScroll, animated);
|
||||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
private float getScrollTargetForDrawable(Drawable target)
|
private float getScrollTargetForDrawable(Drawable target)
|
||||||
{
|
{
|
||||||
// implementation similar to ScrollIntoView but a bit more nuanced.
|
// implementation similar to ScrollIntoView but a bit more nuanced.
|
||||||
return scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre;
|
return (float)(scrollContainer.GetChildPosInContent(target) - scrollContainer.DisplayableContent * scroll_y_centre);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScrollToTop() => scrollContainer.ScrollTo(0);
|
public void ScrollToTop() => scrollContainer.ScrollTo(0);
|
||||||
@ -259,7 +259,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
updateSectionsMargin();
|
updateSectionsMargin();
|
||||||
}
|
}
|
||||||
|
|
||||||
float currentScroll = scrollContainer.Current;
|
float currentScroll = (float)scrollContainer.Current;
|
||||||
|
|
||||||
if (currentScroll != lastKnownScroll)
|
if (currentScroll != lastKnownScroll)
|
||||||
{
|
{
|
||||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default)
|
||||||
{
|
{
|
||||||
UserScrolling = true;
|
UserScrolling = true;
|
||||||
base.OnUserScroll(value, animated, distanceDecay);
|
base.OnUserScroll(value, animated, distanceDecay);
|
||||||
@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
base.ScrollFromMouseEvent(e);
|
base.ScrollFromMouseEvent(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
|
public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null)
|
||||||
{
|
{
|
||||||
UserScrolling = false;
|
UserScrolling = false;
|
||||||
base.ScrollTo(value, animated, distanceDecay);
|
base.ScrollTo(value, animated, distanceDecay);
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Input;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public partial class OsuNumberBox : OsuTextBox
|
public partial class OsuNumberBox : OsuTextBox
|
||||||
{
|
{
|
||||||
protected override bool AllowIme => false;
|
|
||||||
|
|
||||||
public OsuNumberBox()
|
public OsuNumberBox()
|
||||||
{
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Number, false);
|
||||||
|
|
||||||
SelectAllOnFocus = true;
|
SelectAllOnFocus = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ using osu.Game.Localisation;
|
|||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterface
|
namespace osu.Game.Graphics.UserInterface
|
||||||
{
|
{
|
||||||
public partial class OsuPasswordTextBox : OsuTextBox, ISuppressKeyEventLogging
|
public partial class OsuPasswordTextBox : OsuTextBox
|
||||||
{
|
{
|
||||||
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
protected override Drawable GetDrawableCharacter(char c) => new FallingDownContainer
|
||||||
{
|
{
|
||||||
@ -28,12 +28,6 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected override bool AllowUniqueCharacterSamples => false;
|
protected override bool AllowUniqueCharacterSamples => false;
|
||||||
|
|
||||||
protected override bool AllowClipboardExport => false;
|
|
||||||
|
|
||||||
protected override bool AllowWordNavigation => false;
|
|
||||||
|
|
||||||
protected override bool AllowIme => false;
|
|
||||||
|
|
||||||
private readonly CapsWarning warning;
|
private readonly CapsWarning warning;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -41,6 +35,8 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
public OsuPasswordTextBox()
|
public OsuPasswordTextBox()
|
||||||
{
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Password, false);
|
||||||
|
|
||||||
Add(warning = new CapsWarning
|
Add(warning = new CapsWarning
|
||||||
{
|
{
|
||||||
Size = new Vector2(20),
|
Size = new Vector2(20),
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
|
||||||
namespace osu.Game.Graphics.UserInterfaceV2
|
namespace osu.Game.Graphics.UserInterfaceV2
|
||||||
{
|
{
|
||||||
@ -19,6 +20,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
{
|
{
|
||||||
public bool AllowDecimals { get; init; }
|
public bool AllowDecimals { get; init; }
|
||||||
|
|
||||||
|
public InnerNumberBox()
|
||||||
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Number, false);
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool CanAddCharacter(char character)
|
protected override bool CanAddCharacter(char character)
|
||||||
=> char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character));
|
=> char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character));
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,16 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message");
|
public static LocalisableString NotifyOnPrivateMessage => new TranslatableString(getKey(@"notify_on_private_message"), @"Show a notification when you receive a private message");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Show notification popups when friends change status"
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString NotifyOnFriendPresenceChange => new TranslatableString(getKey(@"notify_on_friend_presence_change"), @"Show notification popups when friends change status");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// "Notifications will be shown when friends go online/offline."
|
||||||
|
/// </summary>
|
||||||
|
public static LocalisableString NotifyOnFriendPresenceChangeTooltip => new TranslatableString(getKey(@"notify_on_friend_presence_change_tooltip"), @"Notifications will be shown when friends go online/offline.");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Integrations"
|
/// "Integrations"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -84,6 +94,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString HideCountryFlags => new TranslatableString(getKey(@"hide_country_flags"), @"Hide country flags");
|
public static LocalisableString HideCountryFlags => new TranslatableString(getKey(@"hide_country_flags"), @"Hide country flags");
|
||||||
|
|
||||||
private static string getKey(string key) => $"{prefix}:{key}";
|
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
@ -13,6 +14,7 @@ using System.Threading;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Development;
|
||||||
using osu.Framework.Extensions.ExceptionExtensions;
|
using osu.Framework.Extensions.ExceptionExtensions;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -110,6 +112,15 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
config.BindWith(OsuSetting.UserOnlineStatus, configStatus);
|
||||||
|
|
||||||
|
if (HasLogin)
|
||||||
|
{
|
||||||
|
// Early call to ensure the local user / "logged in" state is correct immediately.
|
||||||
|
setPlaceholderLocalUser();
|
||||||
|
|
||||||
|
// This is required so that Queue() requests during startup sequence don't fail due to "not logged in".
|
||||||
|
state.Value = APIState.Connecting;
|
||||||
|
}
|
||||||
|
|
||||||
localUser.BindValueChanged(u =>
|
localUser.BindValueChanged(u =>
|
||||||
{
|
{
|
||||||
u.OldValue?.Activity.UnbindFrom(activity);
|
u.OldValue?.Activity.UnbindFrom(activity);
|
||||||
@ -193,7 +204,7 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
Debug.Assert(HasLogin);
|
Debug.Assert(HasLogin);
|
||||||
|
|
||||||
// Ensure that we are in an online state. If not, attempt a connect.
|
// Ensure that we are in an online state. If not, attempt to connect.
|
||||||
if (state.Value != APIState.Online)
|
if (state.Value != APIState.Online)
|
||||||
{
|
{
|
||||||
attemptConnect();
|
attemptConnect();
|
||||||
@ -247,17 +258,7 @@ namespace osu.Game.Online.API
|
|||||||
/// <returns>Whether the connection attempt was successful.</returns>
|
/// <returns>Whether the connection attempt was successful.</returns>
|
||||||
private void attemptConnect()
|
private void attemptConnect()
|
||||||
{
|
{
|
||||||
if (localUser.IsDefault)
|
Scheduler.Add(setPlaceholderLocalUser, false);
|
||||||
{
|
|
||||||
// Show a placeholder user if saved credentials are available.
|
|
||||||
// This is useful for storing local scores and showing a placeholder username after starting the game,
|
|
||||||
// until a valid connection has been established.
|
|
||||||
setLocalUser(new APIUser
|
|
||||||
{
|
|
||||||
Username = ProvidedUsername,
|
|
||||||
Status = { Value = configStatus.Value ?? UserStatus.Online }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// save the username at this point, if the user requested for it to be.
|
// save the username at this point, if the user requested for it to be.
|
||||||
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
|
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
|
||||||
@ -339,9 +340,11 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
userReq.Success += me =>
|
userReq.Success += me =>
|
||||||
{
|
{
|
||||||
|
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||||
|
|
||||||
me.Status.Value = configStatus.Value ?? UserStatus.Online;
|
me.Status.Value = configStatus.Value ?? UserStatus.Online;
|
||||||
|
|
||||||
setLocalUser(me);
|
localUser.Value = me;
|
||||||
|
|
||||||
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
state.Value = me.SessionVerified ? APIState.Online : APIState.RequiresSecondFactorAuth;
|
||||||
failureCount = 0;
|
failureCount = 0;
|
||||||
@ -366,6 +369,23 @@ namespace osu.Game.Online.API
|
|||||||
Thread.Sleep(500);
|
Thread.Sleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Show a placeholder user if saved credentials are available.
|
||||||
|
/// This is useful for storing local scores and showing a placeholder username after starting the game,
|
||||||
|
/// until a valid connection has been established.
|
||||||
|
/// </summary>
|
||||||
|
private void setPlaceholderLocalUser()
|
||||||
|
{
|
||||||
|
if (!localUser.IsDefault)
|
||||||
|
return;
|
||||||
|
|
||||||
|
localUser.Value = new APIUser
|
||||||
|
{
|
||||||
|
Username = ProvidedUsername,
|
||||||
|
Status = { Value = configStatus.Value ?? UserStatus.Online }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void Perform(APIRequest request)
|
public void Perform(APIRequest request)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@ -593,7 +613,7 @@ namespace osu.Game.Online.API
|
|||||||
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
|
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
|
||||||
Schedule(() =>
|
Schedule(() =>
|
||||||
{
|
{
|
||||||
setLocalUser(createGuestUser());
|
localUser.Value = createGuestUser();
|
||||||
friends.Clear();
|
friends.Clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -610,8 +630,14 @@ namespace osu.Game.Online.API
|
|||||||
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
||||||
friendsReq.Success += res =>
|
friendsReq.Success += res =>
|
||||||
{
|
{
|
||||||
friends.Clear();
|
var existingFriends = friends.Select(f => f.TargetID).ToHashSet();
|
||||||
friends.AddRange(res);
|
var updatedFriends = res.Select(f => f.TargetID).ToHashSet();
|
||||||
|
|
||||||
|
// Add new friends into local list.
|
||||||
|
friends.AddRange(res.Where(r => !existingFriends.Contains(r.TargetID)));
|
||||||
|
|
||||||
|
// Remove non-friends from local list.
|
||||||
|
friends.RemoveAll(f => !updatedFriends.Contains(f.TargetID));
|
||||||
};
|
};
|
||||||
|
|
||||||
Queue(friendsReq);
|
Queue(friendsReq);
|
||||||
@ -619,8 +645,6 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
private static APIUser createGuestUser() => new GuestUser();
|
private static APIUser createGuestUser() => new GuestUser();
|
||||||
|
|
||||||
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
|
|
||||||
|
|
||||||
protected override void Dispose(bool isDisposing)
|
protected override void Dispose(bool isDisposing)
|
||||||
{
|
{
|
||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
216
osu.Game/Online/FriendPresenceNotifier.cs
Normal file
216
osu.Game/Online/FriendPresenceNotifier.cs
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
// 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.Collections.Specialized;
|
||||||
|
using System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Configuration;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Online.API;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Online.Metadata;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
using osu.Game.Users;
|
||||||
|
|
||||||
|
namespace osu.Game.Online
|
||||||
|
{
|
||||||
|
public partial class FriendPresenceNotifier : Component
|
||||||
|
{
|
||||||
|
[Resolved]
|
||||||
|
private INotificationOverlay notifications { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private IAPIProvider api { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private MetadataClient metadataClient { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ChannelManager channelManager { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private ChatOverlay chatOverlay { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuColour colours { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
|
private readonly Bindable<bool> notifyOnFriendPresenceChange = new BindableBool();
|
||||||
|
|
||||||
|
private readonly IBindableList<APIRelation> friends = new BindableList<APIRelation>();
|
||||||
|
private readonly IBindableDictionary<int, UserPresence> friendStates = new BindableDictionary<int, UserPresence>();
|
||||||
|
|
||||||
|
private readonly HashSet<APIUser> onlineAlertQueue = new HashSet<APIUser>();
|
||||||
|
private readonly HashSet<APIUser> offlineAlertQueue = new HashSet<APIUser>();
|
||||||
|
|
||||||
|
private double? lastOnlineAlertTime;
|
||||||
|
private double? lastOfflineAlertTime;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.NotifyOnFriendPresenceChange, notifyOnFriendPresenceChange);
|
||||||
|
|
||||||
|
friends.BindTo(api.Friends);
|
||||||
|
friends.BindCollectionChanged(onFriendsChanged, true);
|
||||||
|
|
||||||
|
friendStates.BindTo(metadataClient.FriendStates);
|
||||||
|
friendStates.BindCollectionChanged(onFriendStatesChanged, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
alertOnlineUsers();
|
||||||
|
alertOfflineUsers();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFriendsChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyCollectionChangedAction.Add:
|
||||||
|
foreach (APIRelation friend in e.NewItems!.Cast<APIRelation>())
|
||||||
|
{
|
||||||
|
if (friend.TargetUser is not APIUser user)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (friendStates.TryGetValue(friend.TargetID, out _))
|
||||||
|
markUserOnline(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyCollectionChangedAction.Remove:
|
||||||
|
foreach (APIRelation friend in e.OldItems!.Cast<APIRelation>())
|
||||||
|
{
|
||||||
|
if (friend.TargetUser is not APIUser user)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
onlineAlertQueue.Remove(user);
|
||||||
|
offlineAlertQueue.Remove(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onFriendStatesChanged(object? sender, NotifyDictionaryChangedEventArgs<int, UserPresence> e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case NotifyDictionaryChangedAction.Add:
|
||||||
|
foreach ((int friendId, _) in e.NewItems!)
|
||||||
|
{
|
||||||
|
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
|
||||||
|
|
||||||
|
if (friend?.TargetUser is APIUser user)
|
||||||
|
markUserOnline(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NotifyDictionaryChangedAction.Remove:
|
||||||
|
foreach ((int friendId, _) in e.OldItems!)
|
||||||
|
{
|
||||||
|
APIRelation? friend = friends.FirstOrDefault(f => f.TargetID == friendId);
|
||||||
|
|
||||||
|
if (friend?.TargetUser is APIUser user)
|
||||||
|
markUserOffline(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markUserOnline(APIUser user)
|
||||||
|
{
|
||||||
|
if (!offlineAlertQueue.Remove(user))
|
||||||
|
{
|
||||||
|
onlineAlertQueue.Add(user);
|
||||||
|
lastOnlineAlertTime ??= Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markUserOffline(APIUser user)
|
||||||
|
{
|
||||||
|
if (!onlineAlertQueue.Remove(user))
|
||||||
|
{
|
||||||
|
offlineAlertQueue.Add(user);
|
||||||
|
lastOfflineAlertTime ??= Time.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void alertOnlineUsers()
|
||||||
|
{
|
||||||
|
if (onlineAlertQueue.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (lastOnlineAlertTime == null || Time.Current - lastOnlineAlertTime < 1000)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!notifyOnFriendPresenceChange.Value)
|
||||||
|
{
|
||||||
|
lastOnlineAlertTime = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
APIUser? singleUser = onlineAlertQueue.Count == 1 ? onlineAlertQueue.Single() : null;
|
||||||
|
|
||||||
|
notifications.Post(new SimpleNotification
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.UserPlus,
|
||||||
|
Text = $"Online: {string.Join(@", ", onlineAlertQueue.Select(u => u.Username))}",
|
||||||
|
IconColour = colours.Green,
|
||||||
|
Activated = () =>
|
||||||
|
{
|
||||||
|
if (singleUser != null)
|
||||||
|
{
|
||||||
|
channelManager.OpenPrivateChannel(singleUser);
|
||||||
|
chatOverlay.Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onlineAlertQueue.Clear();
|
||||||
|
lastOnlineAlertTime = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void alertOfflineUsers()
|
||||||
|
{
|
||||||
|
if (offlineAlertQueue.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (lastOfflineAlertTime == null || Time.Current - lastOfflineAlertTime < 1000)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!notifyOnFriendPresenceChange.Value)
|
||||||
|
{
|
||||||
|
lastOfflineAlertTime = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifications.Post(new SimpleNotification
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.UserMinus,
|
||||||
|
Text = $"Offline: {string.Join(@", ", offlineAlertQueue.Select(u => u.Username))}",
|
||||||
|
IconColour = colours.Red
|
||||||
|
});
|
||||||
|
|
||||||
|
offlineAlertQueue.Clear();
|
||||||
|
lastOfflineAlertTime = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -375,8 +375,8 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
{
|
{
|
||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
float fadeBottom = scrollContainer.Current + scrollContainer.DrawHeight;
|
float fadeBottom = (float)(scrollContainer.Current + scrollContainer.DrawHeight);
|
||||||
float fadeTop = scrollContainer.Current + LeaderboardScore.HEIGHT;
|
float fadeTop = (float)(scrollContainer.Current + LeaderboardScore.HEIGHT);
|
||||||
|
|
||||||
if (!scrollContainer.IsScrolledToEnd())
|
if (!scrollContainer.IsScrolledToEnd())
|
||||||
fadeBottom -= LeaderboardScore.HEIGHT;
|
fadeBottom -= LeaderboardScore.HEIGHT;
|
||||||
|
@ -101,6 +101,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
private void load(IAPIProvider api, OsuColour colour)
|
private void load(IAPIProvider api, OsuColour colour)
|
||||||
{
|
{
|
||||||
var user = Score.User;
|
var user = Score.User;
|
||||||
|
bool isUserFriend = api.Friends.Any(friend => friend.TargetID == user.OnlineID);
|
||||||
|
|
||||||
statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
|
statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
background = new Box
|
background = new Box
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Colour = user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black,
|
Colour = isUserFriend ? colour.Yellow : (user.OnlineID == api.LocalUser.Value.Id && isOnlineScope ? colour.Green : Color4.Black),
|
||||||
Alpha = background_alpha,
|
Alpha = background_alpha,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -21,6 +21,11 @@ namespace osu.Game.Online.Metadata
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task UserPresenceUpdated(int userId, UserPresence? status);
|
Task UserPresenceUpdated(int userId, UserPresence? status);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delivers and update of the <see cref="UserPresence"/> of a friend with the supplied <paramref name="userId"/>.
|
||||||
|
/// </summary>
|
||||||
|
Task FriendPresenceUpdated(int userId, UserPresence? presence);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delivers an update of the current "daily challenge" status.
|
/// Delivers an update of the current "daily challenge" status.
|
||||||
/// Null value means there is no "daily challenge" currently active.
|
/// Null value means there is no "daily challenge" currently active.
|
||||||
|
@ -42,6 +42,11 @@ namespace osu.Game.Online.Metadata
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract IBindableDictionary<int, UserPresence> UserStates { get; }
|
public abstract IBindableDictionary<int, UserPresence> UserStates { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dictionary keyed by user ID containing all of the <see cref="UserPresence"/> information about currently online friends received from the server.
|
||||||
|
/// </summary>
|
||||||
|
public abstract IBindableDictionary<int, UserPresence> FriendStates { get; }
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract Task UpdateActivity(UserActivity? activity);
|
public abstract Task UpdateActivity(UserActivity? activity);
|
||||||
|
|
||||||
@ -57,6 +62,9 @@ namespace osu.Game.Online.Metadata
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
|
public abstract Task UserPresenceUpdated(int userId, UserPresence? presence);
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public abstract Task FriendPresenceUpdated(int userId, UserPresence? presence);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Daily Challenge
|
#region Daily Challenge
|
||||||
|
@ -26,15 +26,16 @@ namespace osu.Game.Online.Metadata
|
|||||||
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
||||||
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
||||||
|
|
||||||
|
public override IBindableDictionary<int, UserPresence> FriendStates => friendStates;
|
||||||
|
private readonly BindableDictionary<int, UserPresence> friendStates = new BindableDictionary<int, UserPresence>();
|
||||||
|
|
||||||
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
||||||
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
||||||
|
|
||||||
private readonly string endpoint;
|
private readonly string endpoint;
|
||||||
|
|
||||||
private IHubClientConnector? connector;
|
private IHubClientConnector? connector;
|
||||||
|
|
||||||
private Bindable<int> lastQueueId = null!;
|
private Bindable<int> lastQueueId = null!;
|
||||||
|
|
||||||
private IBindable<APIUser> localUser = null!;
|
private IBindable<APIUser> localUser = null!;
|
||||||
private IBindable<UserActivity?> userActivity = null!;
|
private IBindable<UserActivity?> userActivity = null!;
|
||||||
private IBindable<UserStatus?>? userStatus;
|
private IBindable<UserStatus?>? userStatus;
|
||||||
@ -61,6 +62,7 @@ namespace osu.Game.Online.Metadata
|
|||||||
// https://github.com/dotnet/aspnetcore/issues/15198
|
// https://github.com/dotnet/aspnetcore/issues/15198
|
||||||
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
|
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
|
||||||
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
|
connection.On<int, UserPresence?>(nameof(IMetadataClient.UserPresenceUpdated), ((IMetadataClient)this).UserPresenceUpdated);
|
||||||
|
connection.On<int, UserPresence?>(nameof(IMetadataClient.FriendPresenceUpdated), ((IMetadataClient)this).FriendPresenceUpdated);
|
||||||
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
|
connection.On<DailyChallengeInfo?>(nameof(IMetadataClient.DailyChallengeUpdated), ((IMetadataClient)this).DailyChallengeUpdated);
|
||||||
connection.On<MultiplayerRoomScoreSetEvent>(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet);
|
connection.On<MultiplayerRoomScoreSetEvent>(nameof(IMetadataClient.MultiplayerRoomScoreSet), ((IMetadataClient)this).MultiplayerRoomScoreSet);
|
||||||
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
|
connection.On(nameof(IStatefulUserHubClient.DisconnectRequested), ((IMetadataClient)this).DisconnectRequested);
|
||||||
@ -106,6 +108,7 @@ namespace osu.Game.Online.Metadata
|
|||||||
{
|
{
|
||||||
isWatchingUserPresence.Value = false;
|
isWatchingUserPresence.Value = false;
|
||||||
userStates.Clear();
|
userStates.Clear();
|
||||||
|
friendStates.Clear();
|
||||||
dailyChallengeInfo.Value = null;
|
dailyChallengeInfo.Value = null;
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -207,6 +210,19 @@ namespace osu.Game.Online.Metadata
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task FriendPresenceUpdated(int userId, UserPresence? presence)
|
||||||
|
{
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
|
if (presence?.Status != null)
|
||||||
|
friendStates[userId] = presence.Value;
|
||||||
|
else
|
||||||
|
friendStates.Remove(userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public override async Task BeginWatchingUserPresence()
|
public override async Task BeginWatchingUserPresence()
|
||||||
{
|
{
|
||||||
if (connector?.IsConnected.Value != true)
|
if (connector?.IsConnected.Value != true)
|
||||||
|
@ -1136,6 +1136,7 @@ namespace osu.Game
|
|||||||
Add(externalLinkOpener = new ExternalLinkOpener());
|
Add(externalLinkOpener = new ExternalLinkOpener());
|
||||||
Add(new MusicKeyBindingHandler());
|
Add(new MusicKeyBindingHandler());
|
||||||
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
|
Add(new OnlineStatusNotifier(() => ScreenStack.CurrentScreen));
|
||||||
|
Add(new FriendPresenceNotifier());
|
||||||
|
|
||||||
// side overlays which cancel each other.
|
// side overlays which cancel each other.
|
||||||
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay };
|
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, Notifications, FirstRunOverlay };
|
||||||
|
@ -41,13 +41,13 @@ namespace osu.Game.Overlays.Chat
|
|||||||
|
|
||||||
#region Scroll handling
|
#region Scroll handling
|
||||||
|
|
||||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = null)
|
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = null)
|
||||||
{
|
{
|
||||||
base.OnUserScroll(value, animated, distanceDecay);
|
base.OnUserScroll(value, animated, distanceDecay);
|
||||||
updateTrackState();
|
updateTrackState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void ScrollTo(float value, bool animated = true, double? distanceDecay = null)
|
public new void ScrollTo(double value, bool animated = true, double? distanceDecay = null)
|
||||||
{
|
{
|
||||||
base.ScrollTo(value, animated, distanceDecay);
|
base.ScrollTo(value, animated, distanceDecay);
|
||||||
updateTrackState();
|
updateTrackState();
|
||||||
|
@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Chat
|
|||||||
if (chatLine == null)
|
if (chatLine == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
float center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2;
|
double center = scroll.GetChildPosInContent(chatLine, chatLine.DrawSize / 2) - scroll.DisplayableContent / 2;
|
||||||
scroll.ScrollTo(Math.Clamp(center, 0, scroll.ScrollableExtent));
|
scroll.ScrollTo(Math.Clamp(center, 0, scroll.ScrollableExtent));
|
||||||
chatLine.Highlight();
|
chatLine.Highlight();
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ using osu.Framework.Extensions.LocalisationExtensions;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -63,6 +64,7 @@ namespace osu.Game.Overlays.Login
|
|||||||
},
|
},
|
||||||
username = new OsuTextBox
|
username = new OsuTextBox
|
||||||
{
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Username, false),
|
||||||
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
|
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Text = api.ProvidedUsername,
|
Text = api.ProvidedUsername,
|
||||||
|
@ -710,13 +710,13 @@ namespace osu.Game.Overlays.Mods
|
|||||||
|
|
||||||
// the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
|
// the bounds below represent the horizontal range of scroll items to be considered fully visible/active, in the scroll's internal coordinate space.
|
||||||
// note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
|
// note that clamping is applied to the left scroll bound to ensure scrolling past extents does not change the set of active columns.
|
||||||
float leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent);
|
double leftVisibleBound = Math.Clamp(Current, 0, ScrollableExtent);
|
||||||
float rightVisibleBound = leftVisibleBound + DrawWidth;
|
double rightVisibleBound = leftVisibleBound + DrawWidth;
|
||||||
|
|
||||||
// if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
|
// if a movement is occurring at this time, the bounds below represent the full range of columns that the scroll movement will encompass.
|
||||||
// this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
|
// this will be used to ensure that columns do not change state from active to inactive back and forth until they are fully scrolled past.
|
||||||
float leftMovementBound = Math.Min(Current, Target);
|
double leftMovementBound = Math.Min(Current, Target);
|
||||||
float rightMovementBound = Math.Max(Current, Target) + DrawWidth;
|
double rightMovementBound = Math.Max(Current, Target) + DrawWidth;
|
||||||
|
|
||||||
foreach (var column in Child)
|
foreach (var column in Child)
|
||||||
{
|
{
|
||||||
|
@ -136,7 +136,7 @@ namespace osu.Game.Overlays
|
|||||||
{
|
{
|
||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
sidebarContainer.Height = DrawHeight;
|
sidebarContainer.Height = DrawHeight;
|
||||||
sidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
|
sidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadListing(int? year = null)
|
private void loadListing(int? year = null)
|
||||||
|
@ -88,7 +88,7 @@ namespace osu.Game.Overlays
|
|||||||
base.UpdateAfterChildren();
|
base.UpdateAfterChildren();
|
||||||
|
|
||||||
// don't block header by applying padding equal to the visible header height
|
// don't block header by applying padding equal to the visible header height
|
||||||
loadingContainer.Padding = new MarginPadding { Top = Math.Max(0, Header.Height - ScrollFlow.Current) };
|
loadingContainer.Padding = new MarginPadding { Top = (float)Math.Max(0, Header.Height - ScrollFlow.Current) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
public ScrollBackButton Button { get; private set; }
|
public ScrollBackButton Button { get; private set; }
|
||||||
|
|
||||||
private readonly Bindable<float?> lastScrollTarget = new Bindable<float?>();
|
private readonly Bindable<double?> lastScrollTarget = new Bindable<double?>();
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -63,7 +63,7 @@ namespace osu.Game.Overlays
|
|||||||
Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden;
|
Button.State = Target > button_scroll_position || lastScrollTarget.Value != null ? Visibility.Visible : Visibility.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default)
|
protected override void OnUserScroll(double value, bool animated = true, double? distanceDecay = default)
|
||||||
{
|
{
|
||||||
base.OnUserScroll(value, animated, distanceDecay);
|
base.OnUserScroll(value, animated, distanceDecay);
|
||||||
|
|
||||||
@ -112,7 +112,7 @@ namespace osu.Game.Overlays
|
|||||||
private readonly Box background;
|
private readonly Box background;
|
||||||
private readonly SpriteIcon spriteIcon;
|
private readonly SpriteIcon spriteIcon;
|
||||||
|
|
||||||
public Bindable<float?> LastScrollTarget = new Bindable<float?>();
|
public Bindable<double?> LastScrollTarget = new Bindable<double?>();
|
||||||
|
|
||||||
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds();
|
||||||
|
|
||||||
|
@ -29,6 +29,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online
|
|||||||
Current = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage)
|
Current = config.GetBindable<bool>(OsuSetting.NotifyOnPrivateMessage)
|
||||||
},
|
},
|
||||||
new SettingsCheckbox
|
new SettingsCheckbox
|
||||||
|
{
|
||||||
|
LabelText = OnlineSettingsStrings.NotifyOnFriendPresenceChange,
|
||||||
|
TooltipText = OnlineSettingsStrings.NotifyOnFriendPresenceChangeTooltip,
|
||||||
|
Current = config.GetBindable<bool>(OsuSetting.NotifyOnFriendPresenceChange),
|
||||||
|
},
|
||||||
|
new SettingsCheckbox
|
||||||
{
|
{
|
||||||
LabelText = OnlineSettingsStrings.HideCountryFlags,
|
LabelText = OnlineSettingsStrings.HideCountryFlags,
|
||||||
Current = config.GetBindable<bool>(OsuSetting.HideCountryFlags)
|
Current = config.GetBindable<bool>(OsuSetting.HideCountryFlags)
|
||||||
|
@ -5,6 +5,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Settings
|
namespace osu.Game.Overlays.Settings
|
||||||
{
|
{
|
||||||
@ -66,7 +67,10 @@ namespace osu.Game.Overlays.Settings
|
|||||||
|
|
||||||
private partial class OutlinedNumberBox : OutlinedTextBox
|
private partial class OutlinedNumberBox : OutlinedTextBox
|
||||||
{
|
{
|
||||||
protected override bool AllowIme => false;
|
public OutlinedNumberBox()
|
||||||
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Number, false);
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||||
|
|
||||||
|
@ -374,9 +374,10 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeHandler = new SkinEditorChangeHandler(skinComponentsContainer);
|
if (skinComponentsContainer.IsLoaded)
|
||||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
bindChangeHandler(skinComponentsContainer);
|
||||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
else
|
||||||
|
skinComponentsContainer.OnLoadComplete += d => Schedule(() => bindChangeHandler((SkinnableContainer)d));
|
||||||
|
|
||||||
content.Child = new SkinBlueprintContainer(skinComponentsContainer);
|
content.Child = new SkinBlueprintContainer(skinComponentsContainer);
|
||||||
|
|
||||||
@ -418,10 +419,21 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
SelectedComponents.Clear();
|
SelectedComponents.Clear();
|
||||||
placeComponent(component);
|
placeComponent(component);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void bindChangeHandler(SkinnableContainer skinnableContainer)
|
||||||
|
{
|
||||||
|
changeHandler = new SkinEditorChangeHandler(skinnableContainer);
|
||||||
|
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||||
|
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void skinChanged()
|
private void skinChanged()
|
||||||
{
|
{
|
||||||
|
if (skins.EnsureMutableSkin())
|
||||||
|
// Another skin changed event will arrive which will complete the process.
|
||||||
|
return;
|
||||||
|
|
||||||
headerText.Clear();
|
headerText.Clear();
|
||||||
|
|
||||||
headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16));
|
headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16));
|
||||||
@ -439,17 +451,24 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
});
|
});
|
||||||
|
|
||||||
changeHandler?.Dispose();
|
changeHandler?.Dispose();
|
||||||
|
changeHandler = null;
|
||||||
|
|
||||||
skins.EnsureMutableSkin();
|
// Schedule is required to ensure that all layout in `LoadComplete` methods has been completed
|
||||||
|
// before storing an undo state.
|
||||||
|
//
|
||||||
|
// See https://github.com/ppy/osu/blob/8e6a4559e3ae8c9892866cf9cf8d4e8d1b72afd0/osu.Game/Skinning/SkinReloadableDrawable.cs#L76.
|
||||||
|
Schedule(() =>
|
||||||
|
{
|
||||||
var targetContainer = getTarget(selectedTarget.Value);
|
var targetContainer = getTarget(selectedTarget.Value);
|
||||||
|
|
||||||
if (targetContainer != null)
|
if (targetContainer != null)
|
||||||
changeHandler = new SkinEditorChangeHandler(targetContainer);
|
changeHandler = new SkinEditorChangeHandler(targetContainer);
|
||||||
|
|
||||||
hasBegunMutating = true;
|
hasBegunMutating = true;
|
||||||
|
|
||||||
// Reload sidebar components.
|
// Reload sidebar components.
|
||||||
selectedTarget.TriggerChange();
|
selectedTarget.TriggerChange();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Overlays.SkinEditor
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
|
components = new BindableList<ISerialisableDrawable> { BindTarget = firstTarget.Components };
|
||||||
components.BindCollectionChanged((_, _) => SaveState());
|
components.BindCollectionChanged((_, _) => SaveState(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void WriteCurrentStateToStream(MemoryStream stream)
|
protected override void WriteCurrentStateToStream(MemoryStream stream)
|
||||||
|
@ -100,7 +100,7 @@ namespace osu.Game.Overlays
|
|||||||
if (articlePage != null)
|
if (articlePage != null)
|
||||||
{
|
{
|
||||||
articlePage.SidebarContainer.Height = DrawHeight;
|
articlePage.SidebarContainer.Height = DrawHeight;
|
||||||
articlePage.SidebarContainer.Y = Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
|
articlePage.SidebarContainer.Y = (float)Math.Clamp(ScrollFlow.Current - Header.DrawHeight, 0, Math.Max(ScrollFlow.ScrollContent.DrawHeight - DrawHeight - Header.DrawHeight, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes;
|
|||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -371,7 +372,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create all ternary states required to be displayed to the user.
|
/// Create all ternary states required to be displayed to the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
|
protected virtual IEnumerable<Drawable> CreateTernaryButtons() => BlueprintContainer.MainTernaryStates;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
|
||||||
@ -430,7 +431,7 @@ namespace osu.Game.Rulesets.Edit
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (togglesCollection.ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
|
if (togglesCollection.ChildrenOfType<DrawableTernaryButton>().ElementAtOrDefault(rightIndex) is DrawableTernaryButton button)
|
||||||
{
|
{
|
||||||
button.Toggle();
|
button.Toggle();
|
||||||
return true;
|
return true;
|
||||||
|
@ -50,6 +50,9 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
new bool NewCombo { get; set; }
|
new bool NewCombo { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc cref="IHasCombo.ComboOffset"/>
|
||||||
|
new int ComboOffset { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Bindable exposure of <see cref="LastInCombo"/>.
|
/// Bindable exposure of <see cref="LastInCombo"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -84,19 +87,23 @@ namespace osu.Game.Rulesets.Objects.Types
|
|||||||
/// <param name="lastObj">The previous hitobject, or null if this is the first object in the beatmap.</param>
|
/// <param name="lastObj">The previous hitobject, or null if this is the first object in the beatmap.</param>
|
||||||
void UpdateComboInformation(IHasComboInformation? lastObj)
|
void UpdateComboInformation(IHasComboInformation? lastObj)
|
||||||
{
|
{
|
||||||
ComboIndex = lastObj?.ComboIndex ?? 0;
|
int index = lastObj?.ComboIndex ?? 0;
|
||||||
ComboIndexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
int indexWithOffsets = lastObj?.ComboIndexWithOffsets ?? 0;
|
||||||
IndexInCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
int inCurrentCombo = (lastObj?.IndexInCurrentCombo + 1) ?? 0;
|
||||||
|
|
||||||
if (NewCombo || lastObj == null)
|
if (NewCombo || lastObj == null)
|
||||||
{
|
{
|
||||||
IndexInCurrentCombo = 0;
|
inCurrentCombo = 0;
|
||||||
ComboIndex++;
|
index++;
|
||||||
ComboIndexWithOffsets += ComboOffset + 1;
|
indexWithOffsets += ComboOffset + 1;
|
||||||
|
|
||||||
if (lastObj != null)
|
if (lastObj != null)
|
||||||
lastObj.LastInCombo = true;
|
lastObj.LastInCombo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ComboIndex = index;
|
||||||
|
ComboIndexWithOffsets = indexWithOffsets;
|
||||||
|
IndexInCurrentCombo = inCurrentCombo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as
|
/// This miss window should determine how early a hit can be before it is considered for judgement (as opposed to being ignored as
|
||||||
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
|
/// "too far in the future"). It should also define when a forced miss should be triggered (as a result of no user input in time).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
[Description(@"Miss")]
|
[Description(@"Miss")]
|
||||||
[EnumMember(Value = "miss")]
|
[EnumMember(Value = "miss")]
|
||||||
|
@ -0,0 +1,278 @@
|
|||||||
|
// 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 System.Linq;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||||
|
{
|
||||||
|
public partial class NewComboTernaryButton : CompositeDrawable, IHasCurrentValue<TernaryState>
|
||||||
|
{
|
||||||
|
public Bindable<TernaryState> Current
|
||||||
|
{
|
||||||
|
get => current.Current;
|
||||||
|
set => current.Current = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly BindableWithCurrent<TernaryState> current = new BindableWithCurrent<TernaryState>();
|
||||||
|
|
||||||
|
private readonly BindableList<HitObject> selectedHitObjects = new BindableList<HitObject>();
|
||||||
|
private readonly BindableList<Colour4> comboColours = new BindableList<Colour4>();
|
||||||
|
|
||||||
|
private Container mainButtonContainer = null!;
|
||||||
|
private ColourPickerButton pickerButton = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(EditorBeatmap editorBeatmap)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X;
|
||||||
|
AutoSizeAxes = Axes.Y;
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
mainButtonContainer = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Child = new DrawableTernaryButton
|
||||||
|
{
|
||||||
|
Current = Current,
|
||||||
|
Description = "New combo",
|
||||||
|
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pickerButton = new ColourPickerButton
|
||||||
|
{
|
||||||
|
Anchor = Anchor.CentreRight,
|
||||||
|
Origin = Anchor.CentreRight,
|
||||||
|
Alpha = 0,
|
||||||
|
Width = 25,
|
||||||
|
ComboColours = { BindTarget = comboColours }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
selectedHitObjects.BindTo(editorBeatmap.SelectedHitObjects);
|
||||||
|
if (editorBeatmap.BeatmapSkin != null)
|
||||||
|
comboColours.BindTo(editorBeatmap.BeatmapSkin.ComboColours);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
selectedHitObjects.BindCollectionChanged((_, _) => updateState());
|
||||||
|
comboColours.BindCollectionChanged((_, _) => updateState());
|
||||||
|
Current.BindValueChanged(_ => updateState(), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
if (Current.Value == TernaryState.True && selectedHitObjects.Count == 1 && selectedHitObjects.Single() is IHasComboInformation hasCombo && comboColours.Count > 1)
|
||||||
|
{
|
||||||
|
mainButtonContainer.Padding = new MarginPadding { Right = 30 };
|
||||||
|
pickerButton.SelectedHitObject.Value = hasCombo;
|
||||||
|
pickerButton.Alpha = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
mainButtonContainer.Padding = new MarginPadding();
|
||||||
|
pickerButton.Alpha = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class ColourPickerButton : OsuButton, IHasPopover
|
||||||
|
{
|
||||||
|
public BindableList<Colour4> ComboColours { get; } = new BindableList<Colour4>();
|
||||||
|
public Bindable<IHasComboInformation?> SelectedHitObject { get; } = new Bindable<IHasComboInformation?>();
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||||
|
|
||||||
|
private SpriteIcon icon = null!;
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Add(icon = new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.Palette,
|
||||||
|
Size = new Vector2(16),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
});
|
||||||
|
|
||||||
|
Action = this.ShowPopover;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
ComboColours.BindCollectionChanged((_, _) => updateState());
|
||||||
|
SelectedHitObject.BindValueChanged(val =>
|
||||||
|
{
|
||||||
|
if (val.OldValue != null)
|
||||||
|
val.OldValue.ComboIndexWithOffsetsBindable.ValueChanged -= onComboIndexChanged;
|
||||||
|
|
||||||
|
updateState();
|
||||||
|
|
||||||
|
if (val.NewValue != null)
|
||||||
|
val.NewValue.ComboIndexWithOffsetsBindable.ValueChanged += onComboIndexChanged;
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onComboIndexChanged(ValueChangedEvent<int> _) => updateState();
|
||||||
|
|
||||||
|
private void updateState()
|
||||||
|
{
|
||||||
|
Enabled.Value = SelectedHitObject.Value != null;
|
||||||
|
|
||||||
|
if (SelectedHitObject.Value == null || SelectedHitObject.Value.ComboOffset == 0)
|
||||||
|
{
|
||||||
|
BackgroundColour = colourProvider.Background3;
|
||||||
|
icon.Colour = BackgroundColour.Darken(0.5f);
|
||||||
|
icon.Blending = BlendingParameters.Additive;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BackgroundColour = ComboColours[comboIndexFor(SelectedHitObject.Value, ComboColours)];
|
||||||
|
icon.Colour = OsuColour.ForegroundTextColourFor(BackgroundColour);
|
||||||
|
icon.Blending = BlendingParameters.Inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Popover GetPopover() => new ComboColourPalettePopover(ComboColours, SelectedHitObject.Value.AsNonNull(), editorBeatmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private partial class ComboColourPalettePopover : OsuPopover
|
||||||
|
{
|
||||||
|
private readonly IReadOnlyList<Colour4> comboColours;
|
||||||
|
private readonly IHasComboInformation hasComboInformation;
|
||||||
|
private readonly EditorBeatmap editorBeatmap;
|
||||||
|
|
||||||
|
public ComboColourPalettePopover(IReadOnlyList<Colour4> comboColours, IHasComboInformation hasComboInformation, EditorBeatmap editorBeatmap)
|
||||||
|
{
|
||||||
|
this.comboColours = comboColours;
|
||||||
|
this.hasComboInformation = hasComboInformation;
|
||||||
|
this.editorBeatmap = editorBeatmap;
|
||||||
|
|
||||||
|
AllowableAnchors = [Anchor.CentreRight];
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
Debug.Assert(comboColours.Count > 0);
|
||||||
|
var hitObject = hasComboInformation as HitObject;
|
||||||
|
Debug.Assert(hitObject != null);
|
||||||
|
|
||||||
|
FillFlowContainer container;
|
||||||
|
|
||||||
|
Child = container = new FillFlowContainer
|
||||||
|
{
|
||||||
|
Width = 230,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
};
|
||||||
|
|
||||||
|
int selectedColourIndex = comboIndexFor(hasComboInformation, comboColours);
|
||||||
|
|
||||||
|
for (int i = 0; i < comboColours.Count; i++)
|
||||||
|
{
|
||||||
|
int index = i;
|
||||||
|
|
||||||
|
if (getPreviousHitObjectWithCombo(editorBeatmap, hitObject) is IHasComboInformation previousHasCombo
|
||||||
|
&& index == comboIndexFor(previousHasCombo, comboColours)
|
||||||
|
&& !canReuseLastComboColour(editorBeatmap, hitObject))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.Add(new OsuClickableContainer
|
||||||
|
{
|
||||||
|
Size = new Vector2(50),
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 25,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = comboColours[index],
|
||||||
|
},
|
||||||
|
selectedColourIndex == index
|
||||||
|
? new SpriteIcon
|
||||||
|
{
|
||||||
|
Icon = FontAwesome.Solid.Check,
|
||||||
|
Size = new Vector2(24),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Colour = OsuColour.ForegroundTextColourFor(comboColours[index]),
|
||||||
|
}
|
||||||
|
: Empty()
|
||||||
|
},
|
||||||
|
Action = () =>
|
||||||
|
{
|
||||||
|
int comboDifference = index - selectedColourIndex;
|
||||||
|
if (comboDifference == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int newOffset = hasComboInformation.ComboOffset + comboDifference;
|
||||||
|
// `newOffset` must be positive to serialise correctly - this implements the true math "modulus" rather than the built-in "remainder" % op
|
||||||
|
// which can return negative results when the first operand is negative
|
||||||
|
newOffset -= (int)Math.Floor((double)newOffset / comboColours.Count) * comboColours.Count;
|
||||||
|
|
||||||
|
hasComboInformation.ComboOffset = newOffset;
|
||||||
|
editorBeatmap.BeginChange();
|
||||||
|
editorBeatmap.Update((HitObject)hasComboInformation);
|
||||||
|
editorBeatmap.EndChange();
|
||||||
|
this.HidePopover();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IHasComboInformation? getPreviousHitObjectWithCombo(EditorBeatmap editorBeatmap, HitObject hitObject)
|
||||||
|
=> editorBeatmap.HitObjects.TakeWhile(ho => ho != hitObject).LastOrDefault() as IHasComboInformation;
|
||||||
|
|
||||||
|
private static bool canReuseLastComboColour(EditorBeatmap editorBeatmap, HitObject hitObject)
|
||||||
|
{
|
||||||
|
double? closestBreakEnd = editorBeatmap.Breaks.Select(b => b.EndTime)
|
||||||
|
.Where(t => t <= hitObject.StartTime)
|
||||||
|
.OrderBy(t => t)
|
||||||
|
.LastOrDefault();
|
||||||
|
|
||||||
|
if (closestBreakEnd == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return editorBeatmap.HitObjects.FirstOrDefault(ho => ho.StartTime >= closestBreakEnd) == hitObject;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// compare `EditorBeatmapSkin.updateColours()` et al. for reasoning behind the off-by-one index rotation
|
||||||
|
private static int comboIndexFor(IHasComboInformation hasComboInformation, IReadOnlyCollection<Colour4> comboColours)
|
||||||
|
=> (hasComboInformation.ComboIndexWithOffsets + comboColours.Count - 1) % comboColours.Count;
|
||||||
|
}
|
||||||
|
}
|
@ -237,22 +237,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A collection of states which will be displayed to the user in the toolbox.
|
/// A collection of states which will be displayed to the user in the toolbox.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DrawableTernaryButton[] MainTernaryStates { get; private set; }
|
public Drawable[] MainTernaryStates { get; private set; }
|
||||||
|
|
||||||
public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }
|
public SampleBankTernaryButton[] SampleBankTernaryStates { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create all ternary states required to be displayed to the user.
|
/// Create all ternary states required to be displayed to the user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual IEnumerable<DrawableTernaryButton> CreateTernaryButtons()
|
protected virtual IEnumerable<Drawable> CreateTernaryButtons()
|
||||||
{
|
{
|
||||||
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
|
//TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects.
|
||||||
yield return new DrawableTernaryButton
|
yield return new NewComboTernaryButton { Current = NewCombo };
|
||||||
{
|
|
||||||
Current = NewCombo,
|
|
||||||
Description = "New combo",
|
|
||||||
CreateIcon = () => new SpriteIcon { Icon = OsuIcon.EditorNewComboA },
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var kvp in SelectionHandler.SelectionSampleStates)
|
foreach (var kvp in SelectionHandler.SelectionSampleStates)
|
||||||
{
|
{
|
||||||
|
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The timeline's scroll position in the last frame.
|
/// The timeline's scroll position in the last frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float lastScrollPosition;
|
private double lastScrollPosition;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The track time in the last frame.
|
/// The track time in the last frame.
|
||||||
@ -322,7 +322,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double VisibleRange => editorClock.TrackLength / Zoom;
|
public double VisibleRange => editorClock.TrackLength / Zoom;
|
||||||
|
|
||||||
public double TimeAtPosition(float x)
|
public double TimeAtPosition(double x)
|
||||||
{
|
{
|
||||||
return x / Content.DrawWidth * editorClock.TrackLength;
|
return x / Content.DrawWidth * editorClock.TrackLength;
|
||||||
}
|
}
|
||||||
|
@ -182,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None)
|
private void transformZoomTo(float newZoom, float focusPoint, double duration = 0, Easing easing = Easing.None)
|
||||||
=> this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, Current), newZoom, duration, easing));
|
=> this.TransformTo(this.PopulateTransform(new TransformZoom(focusPoint, zoomedContent.DrawWidth, (float)Current), newZoom, duration, easing));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Invoked when <see cref="Zoom"/> has changed.
|
/// Invoked when <see cref="Zoom"/> has changed.
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
|
using osu.Framework.Input;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
@ -136,7 +137,10 @@ namespace osu.Game.Screens.Edit.Setup
|
|||||||
|
|
||||||
private partial class RomanisedTextBox : InnerTextBox
|
private partial class RomanisedTextBox : InnerTextBox
|
||||||
{
|
{
|
||||||
protected override bool AllowIme => false;
|
public RomanisedTextBox()
|
||||||
|
{
|
||||||
|
InputProperties = new TextInputProperties(TextInputType.Text, false);
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool CanAddCharacter(char character)
|
protected override bool CanAddCharacter(char character)
|
||||||
=> MetadataUtils.IsRomanised(character);
|
=> MetadataUtils.IsRomanised(character);
|
||||||
|
@ -69,7 +69,22 @@ namespace osu.Game.Screens.Play
|
|||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
ShowStoryboard.BindValueChanged(_ => initializeStoryboard(true), true);
|
ShowStoryboard.BindValueChanged(show =>
|
||||||
|
{
|
||||||
|
initializeStoryboard(true);
|
||||||
|
|
||||||
|
if (drawableStoryboard != null)
|
||||||
|
{
|
||||||
|
// Regardless of user dim setting, for the time being we need to ensure storyboards are still updated in the background (even if not displayed).
|
||||||
|
// If we don't do this, an intensive storyboard will have a lot of catch-up work to do at the start of a break, causing a huge stutter.
|
||||||
|
//
|
||||||
|
// This can be reconsidered when https://github.com/ppy/osu-framework/issues/6491 is resolved.
|
||||||
|
bool alwaysPresent = show.NewValue;
|
||||||
|
|
||||||
|
Content.AlwaysPresent = alwaysPresent;
|
||||||
|
drawableStoryboard.AlwaysPresent = alwaysPresent;
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,15 +114,15 @@ namespace osu.Game.Screens.Play.HUD
|
|||||||
|
|
||||||
if (requiresScroll && TrackedScore != null)
|
if (requiresScroll && TrackedScore != null)
|
||||||
{
|
{
|
||||||
float scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
double scrollTarget = scroll.GetChildPosInContent(TrackedScore) + TrackedScore.DrawHeight / 2 - scroll.DrawHeight / 2;
|
||||||
|
|
||||||
scroll.ScrollTo(scrollTarget);
|
scroll.ScrollTo(scrollTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
const float panel_height = GameplayLeaderboardScore.PANEL_HEIGHT;
|
||||||
|
|
||||||
float fadeBottom = scroll.Current + scroll.DrawHeight;
|
float fadeBottom = (float)(scroll.Current + scroll.DrawHeight);
|
||||||
float fadeTop = scroll.Current + panel_height;
|
float fadeTop = (float)(scroll.Current + panel_height);
|
||||||
|
|
||||||
if (scroll.IsScrolledToStart()) fadeTop -= panel_height;
|
if (scroll.IsScrolledToStart()) fadeTop -= panel_height;
|
||||||
if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
|
if (!scroll.IsScrolledToEnd()) fadeBottom -= panel_height;
|
||||||
|
@ -165,13 +165,14 @@ namespace osu.Game.Screens.Play.PlayerSettings
|
|||||||
if (setInfo == null) // only the case for tests.
|
if (setInfo == null) // only the case for tests.
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Apply to all difficulties in a beatmap set for now (they generally always share timing).
|
// Apply to all difficulties in a beatmap set if they have the same audio
|
||||||
|
// (they generally always share timing).
|
||||||
foreach (var b in setInfo.Beatmaps)
|
foreach (var b in setInfo.Beatmaps)
|
||||||
{
|
{
|
||||||
BeatmapUserSettings userSettings = b.UserSettings;
|
BeatmapUserSettings userSettings = b.UserSettings;
|
||||||
double val = Current.Value;
|
double val = Current.Value;
|
||||||
|
|
||||||
if (userSettings.Offset != val)
|
if (userSettings.Offset != val && b.AudioEquals(beatmap.Value.BeatmapInfo))
|
||||||
userSettings.Offset = val;
|
userSettings.Offset = val;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -334,7 +334,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
|
|
||||||
private partial class Scroll : OsuScrollContainer
|
private partial class Scroll : OsuScrollContainer
|
||||||
{
|
{
|
||||||
public new float Target => base.Target;
|
public new double Target => base.Target;
|
||||||
|
|
||||||
public Scroll()
|
public Scroll()
|
||||||
: base(Direction.Horizontal)
|
: base(Direction.Horizontal)
|
||||||
@ -344,7 +344,7 @@ namespace osu.Game.Screens.Ranking
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The target that will be scrolled to instantaneously next frame.
|
/// The target that will be scrolled to instantaneously next frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public float? InstantScrollTarget;
|
public double? InstantScrollTarget;
|
||||||
|
|
||||||
protected override void UpdateAfterChildren()
|
protected override void UpdateAfterChildren()
|
||||||
{
|
{
|
||||||
|
@ -611,12 +611,12 @@ namespace osu.Game.Screens.Select
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the lower visible bound with respect to the current scroll position.
|
/// The position of the lower visible bound with respect to the current scroll position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float visibleBottomBound => Scroll.Current + DrawHeight + BleedBottom;
|
private float visibleBottomBound => (float)(Scroll.Current + DrawHeight + BleedBottom);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The position of the upper visible bound with respect to the current scroll position.
|
/// The position of the upper visible bound with respect to the current scroll position.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private float visibleUpperBound => Scroll.Current - BleedTop;
|
private float visibleUpperBound => (float)(Scroll.Current - BleedTop);
|
||||||
|
|
||||||
public void FlushPendingFilterOperations()
|
public void FlushPendingFilterOperations()
|
||||||
{
|
{
|
||||||
@ -1006,7 +1006,7 @@ namespace osu.Game.Screens.Select
|
|||||||
// we take the difference in scroll height and apply to all visible panels.
|
// we take the difference in scroll height and apply to all visible panels.
|
||||||
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
|
// this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer
|
||||||
// to enter clamp-special-case mode where it animates completely differently to normal.
|
// to enter clamp-special-case mode where it animates completely differently to normal.
|
||||||
float scrollChange = scrollTarget.Value - Scroll.Current;
|
float scrollChange = (float)(scrollTarget.Value - Scroll.Current);
|
||||||
Scroll.ScrollTo(scrollTarget.Value, false);
|
Scroll.ScrollTo(scrollTarget.Value, false);
|
||||||
foreach (var i in Scroll)
|
foreach (var i in Scroll)
|
||||||
i.Y += scrollChange;
|
i.Y += scrollChange;
|
||||||
@ -1217,12 +1217,12 @@ namespace osu.Game.Screens.Select
|
|||||||
private const float top_padding = 10;
|
private const float top_padding = 10;
|
||||||
private const float bottom_padding = 70;
|
private const float bottom_padding = 70;
|
||||||
|
|
||||||
protected override float ToScrollbarPosition(float scrollPosition)
|
protected override float ToScrollbarPosition(double scrollPosition)
|
||||||
{
|
{
|
||||||
if (Precision.AlmostEquals(0, ScrollableExtent))
|
if (Precision.AlmostEquals(0, ScrollableExtent))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent);
|
return (float)(top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override float FromScrollbarPosition(float scrollbarPosition)
|
protected override float FromScrollbarPosition(float scrollbarPosition)
|
||||||
@ -1230,7 +1230,7 @@ namespace osu.Game.Screens.Select
|
|||||||
if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
|
if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
|
return (float)(ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Storyboards.Drawables
|
|||||||
|
|
||||||
protected override Container<DrawableStoryboardLayer> Content { get; }
|
protected override Container<DrawableStoryboardLayer> Content { get; }
|
||||||
|
|
||||||
protected override Vector2 DrawScale => new Vector2(Parent!.DrawHeight / 480);
|
protected override Vector2 DrawScale => new Vector2((Parent?.DrawHeight ?? 0) / 480);
|
||||||
|
|
||||||
public override bool RemoveCompletedTransforms => false;
|
public override bool RemoveCompletedTransforms => false;
|
||||||
|
|
||||||
|
@ -22,6 +22,9 @@ namespace osu.Game.Tests.Visual.Metadata
|
|||||||
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
|
||||||
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
|
||||||
|
|
||||||
|
public override IBindableDictionary<int, UserPresence> FriendStates => friendStates;
|
||||||
|
private readonly BindableDictionary<int, UserPresence> friendStates = new BindableDictionary<int, UserPresence>();
|
||||||
|
|
||||||
public override Bindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
public override Bindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
|
||||||
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
|
||||||
|
|
||||||
@ -77,6 +80,16 @@ namespace osu.Game.Tests.Visual.Metadata
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override Task FriendPresenceUpdated(int userId, UserPresence? presence)
|
||||||
|
{
|
||||||
|
if (presence.HasValue)
|
||||||
|
friendStates[userId] = presence.Value;
|
||||||
|
else
|
||||||
|
friendStates.Remove(userId);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
public override Task<BeatmapUpdates> GetChangesSince(int queueId)
|
public override Task<BeatmapUpdates> GetChangesSince(int queueId)
|
||||||
=> Task.FromResult(new BeatmapUpdates(Array.Empty<int>(), queueId));
|
=> Task.FromResult(new BeatmapUpdates(Array.Empty<int>(), queueId));
|
||||||
|
|
||||||
|
@ -35,7 +35,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="20.1.0" />
|
<PackageReference Include="Realm" Version="20.1.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2024.1224.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2025.114.1" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1224.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1224.0" />
|
||||||
<PackageReference Include="Sentry" Version="5.0.0" />
|
<PackageReference Include="Sentry" Version="5.0.0" />
|
||||||
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
|
||||||
|
@ -17,6 +17,6 @@
|
|||||||
<MtouchInterpreter>-all</MtouchInterpreter>
|
<MtouchInterpreter>-all</MtouchInterpreter>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1224.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2025.114.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
Loading…
Reference in New Issue
Block a user