Merge branch 'master' into fix-carousel-centering-on-window-resize

This commit is contained in:
Bartłomiej Dach 2021-01-05 21:30:39 +01:00 committed by GitHub
commit fbbc26d7d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 148 additions and 56 deletions

View File

@ -16,7 +16,9 @@ using osu.Framework.Android;
namespace osu.Android
{
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)]
[IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
[IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })]
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })]
public class OsuGameActivity : AndroidGameActivity
{

View File

@ -95,6 +95,26 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
[Test]
public void TestOutOfOrderStartTimes()
{
var decoder = new LegacyStoryboardDecoder();
using (var resStream = TestResources.OpenResource("out-of-order-starttimes.osb"))
using (var stream = new LineBufferedReader(resStream))
{
var storyboard = decoder.Decode(stream);
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
Assert.AreEqual(2, background.Elements.Count);
Assert.AreEqual(1500, background.Elements[0].StartTime);
Assert.AreEqual(1000, background.Elements[1].StartTime);
Assert.AreEqual(1000, storyboard.EarliestEventTime);
}
}
[Test]
public void TestDecodeVariableWithSuffix()
{

View File

@ -0,0 +1,6 @@
[Events]
//Storyboard Layer 0 (Background)
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1500,1600,0,1
Sprite,Background,TopCentre,"img.jpg",320,240
F,0,1000,1100,0,1

View File

@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerReadyButton : MultiplayerTestScene
{
private MultiplayerReadyButton button;
private BeatmapSetInfo importedSet;
private BeatmapManager beatmaps;
private RulesetStore rulesets;
@ -38,9 +41,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUp]
public new void Setup() => Schedule(() =>
{
var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap);
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
Child = button = new MultiplayerReadyButton
{
@ -51,13 +53,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Value = new PlaylistItem
{
Beatmap = { Value = beatmap },
Ruleset = { Value = beatmap.Ruleset }
Beatmap = { Value = Beatmap.Value.BeatmapInfo },
Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }
}
}
};
});
[Test]
public void TestDeletedBeatmapDisableReady()
{
OsuButton readyButton = null;
AddAssert("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddAssert("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddAssert("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]
public void TestToggleStateWhenNotHost()
{

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Threading.Tasks;
using osu.Framework.Extensions.ExceptionExtensions;
using osu.Framework.Logging;
namespace osu.Game.Extensions
@ -13,13 +17,19 @@ namespace osu.Game.Extensions
/// Avoids unobserved exceptions from being fired.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="logOnError">Whether errors should be logged as important, or silently ignored.</param>
public static void CatchUnobservedExceptions(this Task task, bool logOnError = false)
/// <param name="logAsError">
/// Whether errors should be logged as errors visible to users, or as debug messages.
/// Logging as debug will essentially silence the errors on non-release builds.
/// </param>
public static void CatchUnobservedExceptions(this Task task, bool logAsError = false)
{
task.ContinueWith(t =>
{
if (logOnError)
Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important);
Exception? exception = t.Exception?.AsSingular();
if (logAsError)
Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true);
else
Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug);
}, TaskContinuationOptions.NotOnRanToCompletion);
}
}

View File

@ -31,7 +31,12 @@ namespace osu.Game.Online.API
Acronym = mod.Acronym;
foreach (var (_, property) in mod.GetSettingsSourceProperties())
Settings.Add(property.Name.Underscore(), property.GetValue(mod));
{
var bindable = (IBindable)property.GetValue(mod);
if (!bindable.IsDefault)
Settings.Add(property.Name.Underscore(), bindable);
}
}
public Mod ToMod(Ruleset ruleset)
@ -46,7 +51,7 @@ namespace osu.Game.Online.API
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
((IBindable)property.GetValue(resultMod)).Parse(settingValue);
resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
}
return resultMod;

View File

@ -151,11 +151,11 @@ namespace osu.Game
updateBlockingOverlayFade();
}
public void RemoveBlockingOverlay(OverlayContainer overlay)
public void RemoveBlockingOverlay(OverlayContainer overlay) => Schedule(() =>
{
visibleBlockingOverlays.Remove(overlay);
updateBlockingOverlayFade();
}
});
/// <summary>
/// Close all game-wide overlays.

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarBeatmapListingButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarBeatmapListingButton()
{
Hotkey = GlobalAction.ToggleDirect;

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarChangelogButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(ChangelogOverlay changelog)
{

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarChatButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarChatButton()
{
Hotkey = GlobalAction.ToggleChat;

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarNewsButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(NewsOverlay news)
{

View File

@ -2,11 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarRankingsButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
[BackgroundDependencyLoader(true)]
private void load(RankingsOverlay rankings)
{

View File

@ -2,12 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Overlays.Toolbar
{
public class ToolbarSocialButton : ToolbarOverlayToggleButton
{
protected override Anchor TooltipAnchor => Anchor.TopRight;
public ToolbarSocialButton()
{
Hotkey = GlobalAction.ToggleSocial;

View File

@ -3,7 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json;
@ -84,12 +83,10 @@ namespace osu.Game.Rulesets.Mods
foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties())
{
object bindableObj = property.GetValue(this);
var bindable = (IBindable)property.GetValue(this);
if ((bindableObj as IHasDefaultValue)?.IsDefault == true)
continue;
tooltipTexts.Add($"{attr.Label} {bindableObj}");
if (!bindable.IsDefault)
tooltipTexts.Add($"{attr.Label} {bindable}");
}
return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s)));
@ -136,19 +133,38 @@ namespace osu.Game.Rulesets.Mods
// Copy bindable values across
foreach (var (_, prop) in this.GetSettingsSourceProperties())
{
var origBindable = prop.GetValue(this);
var copyBindable = prop.GetValue(copy);
var origBindable = (IBindable)prop.GetValue(this);
var copyBindable = (IBindable)prop.GetValue(copy);
// The bindables themselves are readonly, so the value must be transferred through the Bindable<T>.Value property.
var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable<object>.Value), BindingFlags.Public | BindingFlags.Instance);
Debug.Assert(valueProperty != null);
valueProperty.SetValue(copyBindable, valueProperty.GetValue(origBindable));
// we only care about changes that have been made away from defaults.
if (!origBindable.IsDefault)
copy.CopyAdjustedSetting(copyBindable, origBindable);
}
return copy;
}
/// <summary>
/// When creating copies or clones of a Mod, this method will be called
/// to copy explicitly adjusted user settings from <paramref name="target"/>.
/// The base implementation will transfer the value via <see cref="Bindable{T}.Parse"/>
/// or by binding and unbinding (if <paramref name="source"/> is an <see cref="IBindable"/>)
/// and should be called unless replaced with custom logic.
/// </summary>
/// <param name="target">The target bindable to apply the adjustment to.</param>
/// <param name="source">The adjustment to apply.</param>
internal virtual void CopyAdjustedSetting(IBindable target, object source)
{
if (source is IBindable sourceBindable)
{
// copy including transfer of default values.
target.BindTo(sourceBindable);
target.UnbindFrom(sourceBindable);
}
else
target.Parse(source);
}
public bool Equals(IMod other) => GetType() == other?.GetType();
}
}

View File

@ -114,6 +114,12 @@ namespace osu.Game.Rulesets.Mods
bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
}
internal override void CopyAdjustedSetting(IBindable target, object source)
{
userChangedSettings[target] = true;
base.CopyAdjustedSetting(target, source);
}
/// <summary>
/// Apply all custom settings to the provided beatmap.
/// </summary>

View File

@ -2,8 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Linq.Expressions;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@ -41,38 +39,21 @@ namespace osu.Game.Screens.OnlinePlay.Components
SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true);
}
private void updateSelectedItem(PlaylistItem item)
{
hasBeatmap = findBeatmap(expr => beatmaps.QueryBeatmap(expr));
}
private void updateSelectedItem(PlaylistItem _) => Scheduler.AddOnce(updateBeatmapState);
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> _) => Scheduler.AddOnce(updateBeatmapState);
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> _) => Scheduler.AddOnce(updateBeatmapState);
private void beatmapUpdated(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{
if (weakSet.NewValue.TryGetTarget(out var set))
{
if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr)))
Schedule(() => hasBeatmap = true);
}
}
private void beatmapRemoved(ValueChangedEvent<WeakReference<BeatmapSetInfo>> weakSet)
{
if (weakSet.NewValue.TryGetTarget(out var set))
{
if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr)))
Schedule(() => hasBeatmap = false);
}
}
private bool findBeatmap(Func<Expression<Func<BeatmapInfo, bool>>, BeatmapInfo> expression)
private void updateBeatmapState()
{
int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID;
string checksum = SelectedItem.Value?.Beatmap.Value?.MD5Hash;
if (beatmapId == null || checksum == null)
return false;
return;
return expression(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum) != null;
var databasedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
hasBeatmap = databasedBeatmap?.BeatmapSet?.DeletePending == false;
}
protected override void Update()

View File

@ -131,7 +131,9 @@ namespace osu.Game.Screens.Play
// if a storyboard is present, it may dictate the appropriate start time by having events in negative time space.
// this is commonly used to display an intro before the audio track start.
startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime);
double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime;
if (firstStoryboardEvent != null)
startTime = Math.Min(startTime, firstStoryboardEvent.Value);
// some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available.
// this is not available as an option in the live editor but can still be applied via .osu editing.

View File

@ -27,7 +27,14 @@ namespace osu.Game.Storyboards
public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable));
public double FirstEventTime => Layers.Min(l => l.Elements.FirstOrDefault()?.StartTime ?? 0);
/// <summary>
/// Across all layers, find the earliest point in time that a storyboard element exists at.
/// Will return null if there are no elements.
/// </summary>
/// <remarks>
/// This iterates all elements and as such should be used sparingly or stored locally.
/// </remarks>
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime;
/// <summary>
/// Depth of the currently front-most storyboard layer, excluding the overlay layer.