1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 17:02:57 +08:00

Merge branch 'master' into fix-editor-difficulty-name-update

This commit is contained in:
Dan Balasescu 2021-10-15 16:58:01 +09:00 committed by GitHub
commit 3604a762d0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 683 additions and 129 deletions

View File

@ -0,0 +1,76 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor
{
/// <summary>
/// Slightly higher than the cutoff for <see cref="Drawable.IsPresent"/>.
/// </summary>
private const float min_alpha = 0.0002f;
private const float transition_duration = 100;
public override string Name => "No Scope";
public override string Acronym => "NS";
public override ModType Type => ModType.Fun;
public override IconUsage? Icon => FontAwesome.Solid.EyeSlash;
public override string Description => "Where's the cursor?";
public override double ScoreMultiplier => 1;
private BindableNumber<int> currentCombo;
private float targetAlpha;
[SettingSource(
"Hidden at combo",
"The combo count at which the cursor becomes completely hidden",
SettingControlType = typeof(SettingsSlider<int, HiddenComboSlider>)
)]
public BindableInt HiddenComboCount { get; } = new BindableInt
{
Default = 10,
Value = 10,
MinValue = 0,
MaxValue = 50,
};
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{
if (HiddenComboCount.Value == 0) return;
currentCombo = scoreProcessor.Combo.GetBoundCopy();
currentCombo.BindValueChanged(combo =>
{
targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value);
}, true);
}
public virtual void Update(Playfield playfield)
{
playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1));
}
}
public class HiddenComboSlider : OsuSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText;
}
}

View File

@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModBarrelRoll(), new OsuModBarrelRoll(),
new OsuModApproachDifferent(), new OsuModApproachDifferent(),
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(),
}; };
case ModType.System: case ModType.System:

View File

@ -0,0 +1,54 @@
// 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.Game.Models;
using osu.Game.Stores;
namespace osu.Game.Tests.Database
{
public class RulesetStoreTests : RealmTest
{
[Test]
public void TestCreateStore()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
});
}
[Test]
public void TestCreateStoreTwiceDoesntAddRulesetsAgain()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
var rulesets2 = new RealmRulesetStore(realmFactory, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First());
Assert.AreEqual(4, realmFactory.Context.All<RealmRuleset>().Count());
});
}
[Test]
public void TestRetrievedRulesetsAreDetached()
{
RunTestWithRealm((realmFactory, storage) =>
{
var rulesets = new RealmRulesetStore(realmFactory, storage);
Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false);
Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false);
});
}
}
}

View File

@ -4,7 +4,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation namespace osu.Game.Tests.Visual.Navigation
@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation
[Test] [Test]
public void TestImportCreatedNotification() public void TestImportCreatedNotification()
{ {
AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType<ImportProgressNotification>().Count() == 1); AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType<ProgressCompletionNotification>().Count() == 1);
} }
} }
} }

View File

@ -1,11 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("create component", () => AddStep("create component", () =>
{ {
LabelledSliderBar<double> component; FillFlowContainer flow;
Child = new Container Child = flow = new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Width = 500, Width = 500,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Child = component = new LabelledSliderBar<double> Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new LabelledSliderBar<double>
{ {
Current = new BindableDouble(5) Current = new BindableDouble(5)
{ {
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 1, Precision = 1,
} },
} Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
},
},
}; };
component.Label = "a sample component"; foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType<OverlayColourScheme>())
component.Description = hasDescription ? "this text describes the component" : string.Empty; {
flow.Add(new OverlayColourContainer(colour)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new LabelledSliderBar<double>
{
Current = new BindableDouble(5)
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
},
Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
}
}); });
} }
});
}
private class OverlayColourContainer : Container
{
[Cached]
private OverlayColourProvider colourProvider;
public OverlayColourContainer(OverlayColourScheme scheme)
{
colourProvider = new OverlayColourProvider(scheme);
}
}
} }
} }

View File

@ -0,0 +1,68 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneSettingsCheckbox : OsuTestScene
{
[TestCase]
public void TestCheckbox()
{
AddStep("create component", () =>
{
FillFlowContainer flow;
Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = "a sample component",
},
},
};
foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType<OverlayColourScheme>())
{
flow.Add(new OverlayColourContainer(colour1)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new SettingsCheckbox
{
LabelText = "a sample component",
}
});
}
});
}
private class OverlayColourContainer : Container
{
[Cached]
private OverlayColourProvider colourProvider;
public OverlayColourContainer(OverlayColourScheme scheme)
{
colourProvider = new OverlayColourProvider(scheme);
}
}
}
}

View File

@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps
} }
} }
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<ILive<BeatmapSetInfo>>> PresentImport { set => beatmapModelManager.PostImport = value; }
/// <summary> /// <summary>
/// Delete a beatmap difficulty. /// Delete a beatmap difficulty.
/// </summary> /// </summary>
@ -338,5 +333,14 @@ namespace osu.Game.Beatmaps
} }
#endregion #endregion
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<ILive<BeatmapSetInfo>>> PostImport
{
set => beatmapModelManager.PostImport = value;
}
#endregion
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam> /// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>, IPostImports<TModel> public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new() where TFileModel : class, INamedFileInfo, new()
{ {

View File

@ -13,7 +13,7 @@ namespace osu.Game.Database
/// A class which handles importing of associated models to the game store. /// A class which handles importing of associated models to the game store.
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>
where TModel : class where TModel : class
{ {
/// <summary> /// <summary>

View File

@ -5,7 +5,6 @@ using System;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
@ -18,7 +17,7 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
/// </summary> /// </summary>
public class RealmContextFactory : Component, IRealmFactory public class RealmContextFactory : IDisposable, IRealmFactory
{ {
private readonly Storage storage; private readonly Storage storage;
@ -79,10 +78,11 @@ namespace osu.Game.Database
/// <returns></returns> /// <returns></returns>
public bool Compact() => Realm.Compact(getConfiguration()); public bool Compact() => Realm.Compact(getConfiguration());
protected override void Update() /// <summary>
/// Perform a blocking refresh on the main realm context.
/// </summary>
public void Refresh()
{ {
base.Update();
lock (contextLock) lock (contextLock)
{ {
if (context?.Refresh() == true) if (context?.Refresh() == true)
@ -92,7 +92,7 @@ namespace osu.Game.Database
public Realm CreateContext() public Realm CreateContext()
{ {
if (IsDisposed) if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory)); throw new ObjectDisposedException(nameof(RealmContextFactory));
try try
@ -132,7 +132,7 @@ namespace osu.Game.Database
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns> /// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
public IDisposable BlockAllOperations() public IDisposable BlockAllOperations()
{ {
if (IsDisposed) if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory)); throw new ObjectDisposedException(nameof(RealmContextFactory));
if (!ThreadSafety.IsUpdateThread) if (!ThreadSafety.IsUpdateThread)
@ -176,21 +176,23 @@ namespace osu.Game.Database
}); });
} }
protected override void Dispose(bool isDisposing) private bool isDisposed;
public void Dispose()
{ {
lock (contextLock) lock (contextLock)
{ {
context?.Dispose(); context?.Dispose();
} }
if (!IsDisposed) if (!isDisposed)
{ {
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock.Wait(); contextCreationLock.Wait();
contextCreationLock.Dispose(); contextCreationLock.Dispose();
}
base.Dispose(isDisposing); isDisposed = true;
}
} }
} }
} }

View File

@ -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; using System;
using JetBrains.Annotations;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -12,31 +13,39 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class Nub : CircularContainer, IHasCurrentValue<bool>, IHasAccentColour public class Nub : CompositeDrawable, IHasCurrentValue<bool>, IHasAccentColour
{ {
public const float COLLAPSED_SIZE = 20; public const float HEIGHT = 15;
public const float EXPANDED_SIZE = 40;
public const float EXPANDED_SIZE = 50;
private const float border_width = 3; private const float border_width = 3;
private const double animate_in_duration = 150; private const double animate_in_duration = 200;
private const double animate_out_duration = 500; private const double animate_out_duration = 500;
private readonly Box fill;
private readonly Container main;
public Nub() public Nub()
{ {
Box fill; Size = new Vector2(EXPANDED_SIZE, HEIGHT);
Size = new Vector2(COLLAPSED_SIZE, 12); InternalChildren = new[]
{
BorderColour = Color4.White; main = new CircularContainer
BorderThickness = border_width; {
BorderColour = Color4.White,
Masking = true; BorderThickness = border_width,
Masking = true,
Children = new[] RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{ {
fill = new Box fill = new Box
{ {
@ -44,31 +53,34 @@ namespace osu.Game.Graphics.UserInterface
Alpha = 0, Alpha = 0,
AlwaysPresent = true, AlwaysPresent = true,
}, },
}; }
},
Current.ValueChanged += filled =>
{
fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
}; };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours) private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{ {
AccentColour = colours.Pink; AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
GlowingAccentColour = colours.PinkLighter; GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;
GlowColour = colours.PinkDarker; GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
EdgeEffect = new EdgeEffectParameters main.EdgeEffect = new EdgeEffectParameters
{ {
Colour = GlowColour.Opacity(0), Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 10, Radius = 8,
Roundness = 8, Roundness = 5,
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(onCurrentValueChanged, true);
}
private bool glowing; private bool glowing;
public bool Glowing public bool Glowing
@ -80,28 +92,17 @@ namespace osu.Game.Graphics.UserInterface
if (value) if (value)
{ {
this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint); main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint);
} }
else else
{ {
FadeEdgeEffectTo(0, animate_out_duration); main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint);
this.FadeColour(AccentColour, animate_out_duration); main.FadeColour(AccentColour, animate_out_duration, Easing.OutQuint);
} }
} }
} }
public bool Expanded
{
set
{
if (value)
this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint);
else
this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint);
}
}
private readonly Bindable<bool> current = new Bindable<bool>(); private readonly Bindable<bool> current = new Bindable<bool>();
public Bindable<bool> Current public Bindable<bool> Current
@ -126,7 +127,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
accentColour = value; accentColour = value;
if (!Glowing) if (!Glowing)
Colour = value; main.Colour = value;
} }
} }
@ -139,7 +140,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
glowingAccentColour = value; glowingAccentColour = value;
if (Glowing) if (Glowing)
Colour = value; main.Colour = value;
} }
} }
@ -152,10 +153,22 @@ namespace osu.Game.Graphics.UserInterface
{ {
glowColour = value; glowColour = value;
var effect = EdgeEffect; var effect = main.EdgeEffect;
effect.Colour = Glowing ? value : value.Opacity(0); effect.Colour = Glowing ? value : value.Opacity(0);
EdgeEffect = effect; main.EdgeEffect = effect;
} }
} }
private void onCurrentValueChanged(ValueChangedEvent<bool> filled)
{
fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
if (filled.NewValue)
main.ResizeWidthTo(1, animate_in_duration, Easing.OutElasticHalf);
else
main.ResizeWidthTo(0.9f, animate_out_duration, Easing.OutElastic);
main.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
}
} }
} }

View File

@ -9,16 +9,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class OsuCheckbox : Checkbox public class OsuCheckbox : Checkbox
{ {
public Color4 CheckedColor { get; set; } = Color4.Cyan;
public Color4 UncheckedColor { get; set; } = Color4.White;
public int FadeDuration { get; set; }
/// <summary> /// <summary>
/// Whether to play sounds when the state changes as a result of user interaction. /// Whether to play sounds when the state changes as a result of user interaction.
/// </summary> /// </summary>
@ -104,14 +99,12 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
Nub.Glowing = true; Nub.Glowing = true;
Nub.Expanded = true;
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
Nub.Glowing = false; Nub.Glowing = false;
Nub.Expanded = false;
base.OnHoverLost(e); base.OnHoverLost(e);
} }

View File

@ -3,11 +3,13 @@
using System; using System;
using System.Globalization; using System.Globalization;
using JetBrains.Annotations;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -16,6 +18,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -52,35 +55,64 @@ namespace osu.Game.Graphics.UserInterface
{ {
accentColour = value; accentColour = value;
leftBox.Colour = value; leftBox.Colour = value;
}
}
private Colour4 backgroundColour;
public Color4 BackgroundColour
{
get => backgroundColour;
set
{
backgroundColour = value;
rightBox.Colour = value; rightBox.Colour = value;
} }
} }
public OsuSliderBar() public OsuSliderBar()
{ {
Height = 12; Height = Nub.HEIGHT;
RangePadding = 20; RangePadding = Nub.EXPANDED_SIZE / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new CircularContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Masking = true,
CornerRadius = 5f,
Children = new Drawable[] Children = new Drawable[]
{ {
leftBox = new Box leftBox = new Box
{ {
Height = 2, Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f), EdgeSmoothness = new Vector2(0, 0.5f),
Position = new Vector2(2, 0),
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}, },
rightBox = new Box rightBox = new Box
{ {
Height = 2, Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f), EdgeSmoothness = new Vector2(0, 0.5f),
Position = new Vector2(-2, 0),
RelativeSizeAxes = Axes.None, RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Alpha = 0.5f, Alpha = 0.5f,
}, },
},
},
},
nubContainer = new Container nubContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -88,7 +120,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X, RelativePositionAxes = Axes.X,
Expanded = true, Current = { Value = true }
}, },
}, },
new HoverClickSounds() new HoverClickSounds()
@ -97,11 +129,12 @@ namespace osu.Game.Graphics.UserInterface
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours) private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{ {
sample = audio.Samples.Get(@"UI/notch-tick"); sample = audio.Samples.Get(@"UI/notch-tick");
AccentColour = colours.Pink; AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f);
} }
protected override void Update() protected override void Update()
@ -119,26 +152,25 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
Nub.Glowing = true; updateGlow();
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
Nub.Glowing = false; updateGlow();
base.OnHoverLost(e); base.OnHoverLost(e);
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override void OnDragEnd(DragEndEvent e)
{ {
Nub.Current.Value = true; updateGlow();
return base.OnMouseDown(e); base.OnDragEnd(e);
} }
protected override void OnMouseUp(MouseUpEvent e) private void updateGlow()
{ {
Nub.Current.Value = false; Nub.Glowing = IsHovered || IsDragged;
base.OnMouseUp(e);
} }
protected override void OnUserChange(T value) protected override void OnUserChange(T value)

View File

@ -2,10 +2,12 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
@ -23,10 +25,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(OsuColour colours) private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours)
{ {
BackgroundColour = colours.Blue3; BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3;
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -374,7 +374,7 @@ namespace osu.Game.Online.Multiplayer
UserJoined?.Invoke(user); UserJoined?.Invoke(user);
RoomUpdated?.Invoke(); RoomUpdated?.Invoke();
}, false); });
} }
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) =>

View File

@ -643,7 +643,7 @@ namespace osu.Game
SkinManager.PostNotification = n => Notifications.Post(n); SkinManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value);
ScoreManager.PostNotification = n => Notifications.Post(n); ScoreManager.PostNotification = n => Notifications.Post(n);
ScoreManager.PostImport = items => PresentScore(items.First().Value); ScoreManager.PostImport = items => PresentScore(items.First().Value);

View File

@ -187,8 +187,6 @@ namespace osu.Game
dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client"));
AddInternal(realmFactory);
dependencies.CacheAs(Storage); dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures"))); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
@ -529,6 +527,7 @@ namespace osu.Game
LocalConfig?.Dispose(); LocalConfig?.Dispose();
contextFactory?.FlushConnections(); contextFactory?.FlushConnections();
realmFactory?.Dispose();
} }
} }
} }

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings
{ {
protected override Drawable CreateControl() => new TSlider protected override Drawable CreateControl() => new TSlider
{ {
Margin = new MarginPadding { Top = 5, Bottom = 5 }, Margin = new MarginPadding { Vertical = 10 },
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X
}; };

View File

@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>, ICanAcceptFiles, IPostImports<ScoreInfo> public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>, ICanAcceptFiles
{ {
private readonly Scheduler scheduler; private readonly Scheduler scheduler;
private readonly Func<BeatmapDifficultyCache> difficulties; private readonly Func<BeatmapDifficultyCache> difficulties;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
{ {
Nub.AccentColour = colours.Yellow; Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter; Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDarker; Nub.GlowColour = colours.YellowDark;
} }
} }
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
AccentColour = colours.Yellow; AccentColour = colours.Yellow;
Nub.AccentColour = colours.Yellow; Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter; Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDarker; Nub.GlowColour = colours.YellowDark;
} }
} }
} }

View File

@ -0,0 +1,263 @@
// 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.IO;
using System.Linq;
using System.Reflection;
using osu.Framework;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Models;
using osu.Game.Rulesets;
#nullable enable
namespace osu.Game.Stores
{
public class RealmRulesetStore : IDisposable
{
private readonly RealmContextFactory realmFactory;
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
private readonly Dictionary<Assembly, Type> loadedAssemblies = new Dictionary<Assembly, Type>();
/// <summary>
/// All available rulesets.
/// </summary>
public IEnumerable<IRulesetInfo> AvailableRulesets => availableRulesets;
private readonly List<IRulesetInfo> availableRulesets = new List<IRulesetInfo>();
public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null)
{
this.realmFactory = realmFactory;
// On android in release configuration assemblies are loaded from the apk directly into memory.
// We cannot read assemblies from cwd, so should check loaded assemblies instead.
loadFromAppDomain();
// This null check prevents Android from attempting to load the rulesets from disk,
// as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android.
// See https://github.com/xamarin/xamarin-android/issues/3489.
if (RuntimeInfo.StartupDirectory != null)
loadFromDisk();
// the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory.
// It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail
// to load as unable to locate the game core assembly.
AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
if (rulesetStorage != null)
loadUserRulesets(rulesetStorage);
addMissingRulesets();
}
/// <summary>
/// Retrieve a ruleset using a known ID.
/// </summary>
/// <param name="id">The ruleset's internal ID.</param>
/// <returns>A ruleset, if available, else null.</returns>
public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id);
/// <summary>
/// Retrieve a ruleset using a known short name.
/// </summary>
/// <param name="shortName">The ruleset's short name.</param>
/// <returns>A ruleset, if available, else null.</returns>
public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName);
private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args)
{
var asm = new AssemblyName(args.Name);
// the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies.
// this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name
// already loaded in the AppDomain.
var domainAssembly = AppDomain.CurrentDomain.GetAssemblies()
// Given name is always going to be equally-or-more qualified than the assembly name.
.Where(a =>
{
string? name = a.GetName().Name;
if (name == null)
return false;
return args.Name.Contains(name, StringComparison.Ordinal);
})
// Pick the greatest assembly version.
.OrderByDescending(a => a.GetName().Version)
.FirstOrDefault();
if (domainAssembly != null)
return domainAssembly;
return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName);
}
private void addMissingRulesets()
{
realmFactory.Context.Write(realm =>
{
var rulesets = realm.All<RealmRuleset>();
List<Ruleset> instances = loadedAssemblies.Values
.Select(r => Activator.CreateInstance(r) as Ruleset)
.Where(r => r != null)
.Select(r => r.AsNonNull())
.ToList();
// add all legacy rulesets first to ensure they have exclusive choice of primary key.
foreach (var r in instances.Where(r => r is ILegacyRuleset))
{
if (realm.All<RealmRuleset>().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.ID) == null)
realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
}
// add any other rulesets which have assemblies present but are not yet in the database.
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
{
if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
{
var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
if (existingSameShortName != null)
{
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
// in such cases, update the instantiation info of the existing entry to point to the new one.
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
}
else
realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID));
}
}
List<RealmRuleset> detachedRulesets = new List<RealmRuleset>();
// perform a consistency check and detach final rulesets from realm for cross-thread runtime usage.
foreach (var r in rulesets)
{
try
{
var type = Type.GetType(r.InstantiationInfo);
if (type == null)
throw new InvalidOperationException(@"Type resolution failure.");
var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo;
if (rInstance == null)
throw new InvalidOperationException(@"Instantiation failure.");
r.Name = rInstance.Name;
r.ShortName = rInstance.ShortName;
r.InstantiationInfo = rInstance.InstantiationInfo;
r.Available = true;
detachedRulesets.Add(r.Clone());
}
catch (Exception ex)
{
r.Available = false;
Logger.Log($"Could not load ruleset {r}: {ex.Message}");
}
}
availableRulesets.AddRange(detachedRulesets);
});
}
private void loadFromAppDomain()
{
foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies())
{
string? rulesetName = ruleset.GetName().Name;
if (rulesetName == null)
continue;
if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests"))
continue;
addRuleset(ruleset);
}
}
private void loadUserRulesets(Storage rulesetStorage)
{
var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
foreach (var ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
}
private void loadFromDisk()
{
try
{
var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll");
foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
loadRulesetFromFile(file);
}
catch (Exception e)
{
Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}");
}
}
private void loadRulesetFromFile(string file)
{
var filename = Path.GetFileNameWithoutExtension(file);
if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
return;
try
{
addRuleset(Assembly.LoadFrom(file));
}
catch (Exception e)
{
Logger.Error(e, $"Failed to load ruleset {filename}");
}
}
private void addRuleset(Assembly assembly)
{
if (loadedAssemblies.ContainsKey(assembly))
return;
// the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799).
// as a failsafe, also compare by FullName.
if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName))
return;
try
{
loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset)));
}
catch (Exception e)
{
Logger.Error(e, $"Failed to add ruleset {assembly}");
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly;
}
}
}

View File

@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false) public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
{ {
var roomUser = new MultiplayerRoomUser(user.Id) { User = user }; var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
((IMultiplayerClient)this).UserJoined(roomUser);
addUser(roomUser);
if (markAsPlaying) if (markAsPlaying)
PlayingUserIds.Add(user.Id); PlayingUserIds.Add(user.Id);
@ -61,7 +62,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
return roomUser; return roomUser;
} }
public void AddNullUser() => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID)); public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID));
private void addUser(MultiplayerRoomUser user)
{
((IMultiplayerClient)this).UserJoined(user).Wait();
// We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation.
Scheduler.Update();
}
public void RemoveUser(User user) public void RemoveUser(User user)
{ {