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 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
new OsuModBarrelRoll(),
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
};
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 NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation
@ -25,7 +25,7 @@ public override void SetUpSteps()
[Test]
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.
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
@ -19,28 +23,62 @@ private void createSliderBar(bool hasDescription = false)
{
AddStep("create component", () =>
{
LabelledSliderBar<double> component;
FillFlowContainer flow;
Child = new Container
Child = flow = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
Child = component = new LabelledSliderBar<double>
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Current = new BindableDouble(5)
new LabelledSliderBar<double>
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
}
}
Current = new BindableDouble(5)
{
MinValue = 0,
MaxValue = 10,
Precision = 1,
},
Label = "a sample component",
Description = hasDescription ? "this text describes the component" : string.Empty,
},
},
};
component.Label = "a sample component";
component.Description = hasDescription ? "this text describes the component" : string.Empty;
foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType<OverlayColourScheme>())
{
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 @@ public Action<Notification> PostNotification
}
}
/// <summary>
/// Fired when the user requests to view the resulting import.
/// </summary>
public Action<IEnumerable<ILive<BeatmapSetInfo>>> PresentImport { set => beatmapModelManager.PostImport = value; }
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>
@ -338,5 +333,14 @@ public void Dispose()
}
#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>
/// <typeparam name="TModel">The model 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 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.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>
where TModel : class
{
/// <summary>

View File

@ -5,7 +5,6 @@
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Statistics;
@ -18,7 +17,7 @@ namespace osu.Game.Database
/// <summary>
/// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage.
/// </summary>
public class RealmContextFactory : Component, IRealmFactory
public class RealmContextFactory : IDisposable, IRealmFactory
{
private readonly Storage storage;
@ -79,10 +78,11 @@ public RealmContextFactory(Storage storage, string filename)
/// <returns></returns>
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)
{
if (context?.Refresh() == true)
@ -92,7 +92,7 @@ protected override void Update()
public Realm CreateContext()
{
if (IsDisposed)
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
try
@ -132,7 +132,7 @@ private void onMigration(Migration migration, ulong lastSchemaVersion)
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
public IDisposable BlockAllOperations()
{
if (IsDisposed)
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
if (!ThreadSafety.IsUpdateThread)
@ -176,21 +176,23 @@ public IDisposable BlockAllOperations()
});
}
protected override void Dispose(bool isDisposing)
private bool isDisposed;
public void Dispose()
{
lock (contextLock)
{
context?.Dispose();
}
if (!IsDisposed)
if (!isDisposed)
{
// intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal.
contextCreationLock.Wait();
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.
using System;
using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@ -12,63 +13,74 @@
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Overlays;
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 EXPANDED_SIZE = 40;
public const float HEIGHT = 15;
public const float EXPANDED_SIZE = 50;
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 readonly Box fill;
private readonly Container main;
public Nub()
{
Box fill;
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
Size = new Vector2(COLLAPSED_SIZE, 12);
BorderColour = Color4.White;
BorderThickness = border_width;
Masking = true;
Children = new[]
InternalChildren = new[]
{
fill = new Box
main = new CircularContainer
{
BorderColour = Color4.White,
BorderThickness = border_width,
Masking = true,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Children = new Drawable[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
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]
private void load(OsuColour colours)
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
{
AccentColour = colours.Pink;
GlowingAccentColour = colours.PinkLighter;
GlowColour = colours.PinkDarker;
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;
GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
EdgeEffect = new EdgeEffectParameters
main.EdgeEffect = new EdgeEffectParameters
{
Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow,
Radius = 10,
Roundness = 8,
Radius = 8,
Roundness = 5,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(onCurrentValueChanged, true);
}
private bool glowing;
public bool Glowing
@ -80,28 +92,17 @@ public bool Glowing
if (value)
{
this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint);
main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint);
main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint);
}
else
{
FadeEdgeEffectTo(0, animate_out_duration);
this.FadeColour(AccentColour, animate_out_duration);
main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint);
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>();
public Bindable<bool> Current
@ -126,7 +127,7 @@ public Color4 AccentColour
{
accentColour = value;
if (!Glowing)
Colour = value;
main.Colour = value;
}
}
@ -139,7 +140,7 @@ public Color4 GlowingAccentColour
{
glowingAccentColour = value;
if (Glowing)
Colour = value;
main.Colour = value;
}
}
@ -152,10 +153,22 @@ public Color4 GlowColour
{
glowColour = value;
var effect = EdgeEffect;
var effect = main.EdgeEffect;
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.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public class OsuCheckbox : Checkbox
{
public Color4 CheckedColor { get; set; } = Color4.Cyan;
public Color4 UncheckedColor { get; set; } = Color4.White;
public int FadeDuration { get; set; }
/// <summary>
/// Whether to play sounds when the state changes as a result of user interaction.
/// </summary>
@ -104,14 +99,12 @@ private void load(AudioManager audio)
protected override bool OnHover(HoverEvent e)
{
Nub.Glowing = true;
Nub.Expanded = true;
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
Nub.Glowing = false;
Nub.Expanded = false;
base.OnHoverLost(e);
}

View File

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

View File

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

View File

@ -374,7 +374,7 @@ async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user)
UserJoined?.Invoke(user);
RoomUpdated?.Invoke();
}, false);
});
}
Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) =>

View File

@ -643,7 +643,7 @@ protected override void LoadComplete()
SkinManager.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.PostImport = items => PresentScore(items.First().Value);

View File

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

View File

@ -19,7 +19,7 @@ public class SettingsSlider<TValue, TSlider> : SettingsItem<TValue>
{
protected override Drawable CreateControl() => new TSlider
{
Margin = new MarginPadding { Top = 5, Bottom = 5 },
Margin = new MarginPadding { Vertical = 10 },
RelativeSizeAxes = Axes.X
};

View File

@ -25,7 +25,7 @@
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 Func<BeatmapDifficultyCache> difficulties;

View File

@ -14,7 +14,7 @@ private void load(OsuColour colours)
{
Nub.AccentColour = colours.Yellow;
Nub.GlowingAccentColour = colours.YellowLighter;
Nub.GlowColour = colours.YellowDarker;
Nub.GlowColour = colours.YellowDark;
}
}
}

View File

@ -29,7 +29,7 @@ private void load(OsuColour colours)
AccentColour = colours.Yellow;
Nub.AccentColour = colours.Yellow;
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 @@ public TestMultiplayerClient(TestRequestHandlingMultiplayerRoomManager roomManag
public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
{
var roomUser = new MultiplayerRoomUser(user.Id) { User = user };
((IMultiplayerClient)this).UserJoined(roomUser);
addUser(roomUser);
if (markAsPlaying)
PlayingUserIds.Add(user.Id);
@ -61,7 +62,15 @@ public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false)
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)
{