1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 10:42:54 +08:00

Merge branch 'master' into spectator-replay-watcher

This commit is contained in:
Bartłomiej Dach 2020-11-01 15:53:37 +01:00
commit 23d9fa4dfa
12 changed files with 406 additions and 29 deletions

View File

@ -821,15 +821,13 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
await manager.Import(temp); var importedSet = await manager.Import(temp);
var imported = manager.GetAllUsableBeatmapSets();
ensureLoaded(osu); ensureLoaded(osu);
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return imported.LastOrDefault(); return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID);
} }
private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu)

View File

@ -0,0 +1,196 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.Visual.Background
{
public class TestSceneSeasonalBackgroundLoader : ScreenTestScene
{
[Resolved]
private OsuConfigManager config { get; set; }
[Resolved]
private SessionStatics statics { get; set; }
[Cached(typeof(LargeTextureStore))]
private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore();
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private SeasonalBackgroundLoader backgroundLoader;
private Container backgroundContainer;
// in real usages these would be online URLs, but correct execution of this test
// shouldn't be coupled to existence of online assets.
private static readonly List<string> seasonal_background_urls = new List<string>
{
"Backgrounds/bg2",
"Backgrounds/bg4",
"Backgrounds/bg3"
};
[BackgroundDependencyLoader]
private void load(LargeTextureStore wrappedStore)
{
textureStore.AddStore(wrappedStore);
Add(backgroundContainer = new Container
{
RelativeSizeAxes = Axes.Both
});
}
[SetUp]
public void SetUp() => Schedule(() =>
{
// reset API response in statics to avoid test crosstalk.
statics.Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
textureStore.PerformedLookups.Clear();
dummyAPI.SetState(APIState.Online);
backgroundContainer.Clear();
});
[TestCase(-5)]
[TestCase(5)]
public void TestAlwaysSeasonal(int daysOffset)
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(daysOffset));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Always);
createLoader();
for (int i = 0; i < 4; ++i)
loadNextBackground();
AddAssert("all backgrounds cycled", () => new HashSet<string>(textureStore.PerformedLookups).SetEquals(seasonal_background_urls));
}
[TestCase(-5)]
[TestCase(5)]
public void TestNeverSeasonal(int daysOffset)
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(daysOffset));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Never);
createLoader();
assertNoBackgrounds();
}
[Test]
public void TestSometimesInSeason()
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(5));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Sometimes);
createLoader();
assertAnyBackground();
}
[Test]
public void TestSometimesOutOfSeason()
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(-10));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Sometimes);
createLoader();
assertNoBackgrounds();
}
[Test]
public void TestDelayedConnectivity()
{
registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30));
setSeasonalBackgroundMode(SeasonalBackgroundMode.Always);
AddStep("go offline", () => dummyAPI.SetState(APIState.Offline));
createLoader();
assertNoBackgrounds();
AddStep("go online", () => dummyAPI.SetState(APIState.Online));
assertAnyBackground();
}
private void registerBackgroundsResponse(DateTimeOffset endDate)
=> AddStep("setup request handler", () =>
{
dummyAPI.HandleRequest = request =>
{
if (dummyAPI.State.Value != APIState.Online || !(request is GetSeasonalBackgroundsRequest backgroundsRequest))
return;
backgroundsRequest.TriggerSuccess(new APISeasonalBackgrounds
{
Backgrounds = seasonal_background_urls.Select(url => new APISeasonalBackground { Url = url }).ToList(),
EndDate = endDate
});
};
});
private void setSeasonalBackgroundMode(SeasonalBackgroundMode mode)
=> AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode));
private void createLoader()
=> AddStep("create loader", () =>
{
if (backgroundLoader != null)
Remove(backgroundLoader);
Add(backgroundLoader = new SeasonalBackgroundLoader());
});
private void loadNextBackground()
{
SeasonalBackground background = null;
AddStep("create next background", () =>
{
background = backgroundLoader.LoadNextBackground();
LoadComponentAsync(background, bg => backgroundContainer.Child = bg);
});
AddUntilStep("background loaded", () => background.IsLoaded);
}
private void assertAnyBackground()
{
loadNextBackground();
AddAssert("background looked up", () => textureStore.PerformedLookups.Any());
}
private void assertNoBackgrounds()
{
AddAssert("no background available", () => backgroundLoader.LoadNextBackground() == null);
AddAssert("no lookups performed", () => !textureStore.PerformedLookups.Any());
}
private class LookupLoggingTextureStore : LargeTextureStore
{
public List<string> PerformedLookups { get; } = new List<string>();
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
{
PerformedLookups.Add(name);
return base.Get(name, wrapModeS, wrapModeT);
}
}
}
}

View File

@ -131,6 +131,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.IntroSequence, IntroSequence.Triangles); Set(OsuSetting.IntroSequence, IntroSequence.Triangles);
Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin);
Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes);
} }
public OsuConfigManager(Storage storage) public OsuConfigManager(Storage storage)
@ -240,5 +241,6 @@ namespace osu.Game.Configuration
HitLighting, HitLighting,
MenuBackgroundSource, MenuBackgroundSource,
GameplayDisableWinKey, GameplayDisableWinKey,
SeasonalBackgroundMode
} }
} }

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Configuration
{
public enum SeasonalBackgroundMode
{
/// <summary>
/// Seasonal backgrounds are shown regardless of season, if at all available.
/// </summary>
Always,
/// <summary>
/// Seasonal backgrounds are shown only during their corresponding season.
/// </summary>
Sometimes,
/// <summary>
/// Seasonal backgrounds are never shown.
/// </summary>
Never
}
}

View File

@ -1,6 +1,8 @@
// 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.Game.Online.API.Requests.Responses;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
/// <summary> /// <summary>
@ -12,12 +14,19 @@ namespace osu.Game.Configuration
{ {
Set(Static.LoginOverlayDisplayed, false); Set(Static.LoginOverlayDisplayed, false);
Set(Static.MutedAudioNotificationShownOnce, false); Set(Static.MutedAudioNotificationShownOnce, false);
Set<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
} }
} }
public enum Static public enum Static
{ {
LoginOverlayDisplayed, LoginOverlayDisplayed,
MutedAudioNotificationShownOnce MutedAudioNotificationShownOnce,
/// <summary>
/// Info about seasonal backgrounds available fetched from API - see <see cref="APISeasonalBackgrounds"/>.
/// Value under this lookup can be <c>null</c> if there are no backgrounds available (or API is not reachable).
/// </summary>
SeasonalBackgrounds,
} }
} }

View File

@ -0,0 +1,103 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Graphics.Backgrounds
{
public class SeasonalBackgroundLoader : Component
{
/// <summary>
/// Fired when background should be changed due to receiving backgrounds from API
/// or when the user setting is changed (as it might require unloading the seasonal background).
/// </summary>
public event Action SeasonalBackgroundChanged;
[Resolved]
private IAPIProvider api { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private Bindable<SeasonalBackgroundMode> seasonalBackgroundMode;
private Bindable<APISeasonalBackgrounds> seasonalBackgrounds;
private int current;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config, SessionStatics sessionStatics)
{
seasonalBackgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
seasonalBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
seasonalBackgrounds.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
apiState.BindTo(api.State);
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
}
private void fetchSeasonalBackgrounds(ValueChangedEvent<APIState> stateChanged)
{
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
return;
var request = new GetSeasonalBackgroundsRequest();
request.Success += response =>
{
seasonalBackgrounds.Value = response;
current = RNG.Next(0, response.Backgrounds?.Count ?? 0);
};
api.PerformAsync(request);
}
public SeasonalBackground LoadNextBackground()
{
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never
|| (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason))
{
return null;
}
var backgrounds = seasonalBackgrounds.Value?.Backgrounds;
if (backgrounds == null || !backgrounds.Any())
return null;
current = (current + 1) % backgrounds.Count;
string url = backgrounds[current].Url;
return new SeasonalBackground(url);
}
private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate;
}
[LongRunningLoad]
public class SeasonalBackground : Background
{
private readonly string url;
private const string fallback_texture_name = @"Backgrounds/bg1";
public SeasonalBackground(string url)
{
this.url = url;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures)
{
Sprite.Texture = textures.Get(url) ?? textures.Get(fallback_texture_name);
// ensure we're not loading in without a transition.
this.FadeInFromZero(200, Easing.InOutSine);
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetSeasonalBackgroundsRequest : APIRequest<APISeasonalBackgrounds>
{
protected override string Target => @"seasonal-backgrounds";
}
}

View File

@ -0,0 +1,24 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APISeasonalBackgrounds
{
[JsonProperty("ends_at")]
public DateTimeOffset EndDate;
[JsonProperty("backgrounds")]
public List<APISeasonalBackground> Backgrounds { get; set; }
}
public class APISeasonalBackground
{
[JsonProperty("url")]
public string Url { get; set; }
}
}

View File

@ -39,6 +39,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
LabelText = "Background source", LabelText = "Background source",
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource), Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
Items = Enum.GetValues(typeof(BackgroundSource)).Cast<BackgroundSource>() Items = Enum.GetValues(typeof(BackgroundSource)).Cast<BackgroundSource>()
},
new SettingsDropdown<SeasonalBackgroundMode>
{
LabelText = "Seasonal backgrounds",
Current = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode),
Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast<SeasonalBackgroundMode>()
} }
}; };
} }

View File

@ -1,6 +1,7 @@
// 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.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -25,6 +26,7 @@ namespace osu.Game.Screens.Backgrounds
private Bindable<Skin> skin; private Bindable<Skin> skin;
private Bindable<BackgroundSource> mode; private Bindable<BackgroundSource> mode;
private Bindable<IntroSequence> introSequence; private Bindable<IntroSequence> introSequence;
private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader();
[Resolved] [Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } private IBindable<WorkingBeatmap> beatmap { get; set; }
@ -42,15 +44,18 @@ namespace osu.Game.Screens.Backgrounds
mode = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource); mode = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource);
introSequence = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence); introSequence = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence);
AddInternal(seasonalBackgroundLoader);
user.ValueChanged += _ => Next(); user.ValueChanged += _ => Next();
skin.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next();
mode.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next();
beatmap.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next();
introSequence.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next();
seasonalBackgroundLoader.SeasonalBackgroundChanged += Next;
currentDisplay = RNG.Next(0, background_count); currentDisplay = RNG.Next(0, background_count);
display(createBackground()); Next();
} }
private void display(Background newBackground) private void display(Background newBackground)
@ -63,11 +68,14 @@ namespace osu.Game.Screens.Backgrounds
} }
private ScheduledDelegate nextTask; private ScheduledDelegate nextTask;
private CancellationTokenSource cancellationTokenSource;
public void Next() public void Next()
{ {
nextTask?.Cancel(); nextTask?.Cancel();
nextTask = Scheduler.AddDelayed(() => { LoadComponentAsync(createBackground(), display); }, 100); cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
nextTask = Scheduler.AddDelayed(() => LoadComponentAsync(createBackground(), display, cancellationTokenSource.Token), 100);
} }
private Background createBackground() private Background createBackground()
@ -75,6 +83,14 @@ namespace osu.Game.Screens.Backgrounds
Background newBackground; Background newBackground;
string backgroundName; string backgroundName;
var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground();
if (seasonalBackground != null)
{
seasonalBackground.Depth = currentDisplay;
return seasonalBackground;
}
switch (introSequence.Value) switch (introSequence.Value)
{ {
case IntroSequence.Welcome: case IntroSequence.Welcome:

View File

@ -210,10 +210,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
if (DragBox.State == Visibility.Visible) if (DragBox.State == Visibility.Visible)
{
DragBox.Hide(); DragBox.Hide();
SelectionHandler.UpdateVisibility();
}
} }
protected override bool OnKeyDown(KeyDownEvent e) protected override bool OnKeyDown(KeyDownEvent e)
@ -352,11 +349,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Selects all <see cref="SelectionBlueprint"/>s. /// Selects all <see cref="SelectionBlueprint"/>s.
/// </summary> /// </summary>
private void selectAll() private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select());
{
SelectionBlueprints.ToList().ForEach(m => m.Select());
SelectionHandler.UpdateVisibility();
}
/// <summary> /// <summary>
/// Deselects all selected <see cref="SelectionBlueprint"/>s. /// Deselects all selected <see cref="SelectionBlueprint"/>s.

View File

@ -201,8 +201,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
// there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once. // there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once.
if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject))
EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
UpdateVisibility();
} }
/// <summary> /// <summary>
@ -214,8 +212,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
selectedBlueprints.Remove(blueprint); selectedBlueprints.Remove(blueprint);
EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
UpdateVisibility();
} }
/// <summary> /// <summary>
@ -254,23 +250,18 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Updates whether this <see cref="SelectionHandler"/> is visible. /// Updates whether this <see cref="SelectionHandler"/> is visible.
/// </summary> /// </summary>
internal void UpdateVisibility() private void updateVisibility()
{ {
int count = selectedBlueprints.Count; int count = selectedBlueprints.Count;
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
if (count > 0) this.FadeTo(count > 0 ? 1 : 0);
{
Show();
OnSelectionChanged(); OnSelectionChanged();
} }
else
Hide();
}
/// <summary> /// <summary>
/// Triggered whenever more than one object is selected, on each change. /// Triggered whenever the set of selected objects changes.
/// Should update the selection box's state to match supported operations. /// Should update the selection box's state to match supported operations.
/// </summary> /// </summary>
protected virtual void OnSelectionChanged() protected virtual void OnSelectionChanged()
@ -421,7 +412,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
// bring in updates from selection changes // bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates();
EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates(); EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) =>
{
Scheduler.AddOnce(updateVisibility);
UpdateTernaryStates();
};
} }
/// <summary> /// <summary>