diff --git a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs index 0bec02c488..3de2dc72bb 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrawableHitObject.cs @@ -3,6 +3,8 @@ using NUnit.Framework; using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Tests.Visual; @@ -70,15 +72,46 @@ namespace osu.Game.Tests.Gameplay AddAssert("Lifetime is changed", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == 1000); } + [Test] + public void TestLifetimeUpdatedOnDefaultApplied() + { + TestLifetimeEntry entry = null; + AddStep("Create entry", () => entry = new TestLifetimeEntry(new HitObject()) { LifetimeStart = 1 }); + AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + AddAssert("Lifetime is updated", () => entry.LifetimeStart == -TestLifetimeEntry.INITIAL_LIFETIME_OFFSET); + + TestDrawableHitObject dho = null; + AddStep("Create DHO", () => + { + dho = new TestDrawableHitObject(null); + dho.Apply(entry); + Child = dho; + dho.SetLifetimeStartOnApply = true; + }); + AddStep("ApplyDefaults", () => entry.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty())); + AddAssert("Lifetime is correct", () => dho.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY && entry.LifetimeStart == TestDrawableHitObject.LIFETIME_ON_APPLY); + } + private class TestDrawableHitObject : DrawableHitObject { public const double INITIAL_LIFETIME_OFFSET = 100; + public const double LIFETIME_ON_APPLY = 222; protected override double InitialLifetimeOffset => INITIAL_LIFETIME_OFFSET; + public bool SetLifetimeStartOnApply; + public TestDrawableHitObject(HitObject hitObject) : base(hitObject) { } + + protected override void OnApply() + { + base.OnApply(); + + if (SetLifetimeStartOnApply) + LifetimeStart = LIFETIME_ON_APPLY; + } } private class TestLifetimeEntry : HitObjectLifetimeEntry diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index dd5e01adbb..07162c3cd1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -31,8 +31,6 @@ namespace osu.Game.Tests.Visual.Editing public override void SetUpSteps() { - AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); - base.SetUpSteps(); // if we save a beatmap with a hash collision, things fall over. @@ -40,6 +38,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); } + protected override void LoadEditor() + { + Beatmap.Value = new DummyWorkingBeatmap(Audio, null); + base.LoadEditor(); + } + [Test] public void TestCreateNewBeatmap() { diff --git a/osu.Game/Input/OsuUserInputManager.cs b/osu.Game/Input/OsuUserInputManager.cs new file mode 100644 index 0000000000..621f1e1fc9 --- /dev/null +++ b/osu.Game/Input/OsuUserInputManager.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Input; +using osuTK.Input; + +namespace osu.Game.Input +{ + public class OsuUserInputManager : UserInputManager + { + internal OsuUserInputManager() + { + } + + protected override MouseButtonEventManager CreateButtonEventManagerFor(MouseButton button) + { + switch (button) + { + case MouseButton.Right: + return new RightMouseManager(button); + } + + return base.CreateButtonEventManagerFor(button); + } + + private class RightMouseManager : MouseButtonEventManager + { + public RightMouseManager(MouseButton button) + : base(button) + { + } + + public override bool EnableDrag => true; // allow right-mouse dragging for absolute scroll in scroll containers. + public override bool EnableClick => false; + public override bool ChangeFocusOnClick => false; + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index a4fc963328..2546374b21 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -235,6 +235,9 @@ namespace osu.Game.Online.Spectator { Debug.Assert(ThreadSafety.IsUpdateThread); + if (!IsPlaying) + return; + if (frame is IConvertibleReplayFrame convertible) pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3c143c1db9..918f231a19 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; -using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -41,7 +40,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Utils; -using osuTK.Input; using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game @@ -51,78 +49,19 @@ namespace osu.Game /// Unlike , this class will not load any kind of UI, allowing it to be used /// for provide dependencies to test cases without interfering with them. /// - public class OsuGameBase : Framework.Game, ICanAcceptFiles + public partial class OsuGameBase : Framework.Game, ICanAcceptFiles { - public const string CLIENT_STREAM_NAME = "lazer"; + public const string CLIENT_STREAM_NAME = @"lazer"; public const int SAMPLE_CONCURRENCY = 6; + /// + /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. + /// + internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5; + public bool UseDevelopmentServer { get; } - protected OsuConfigManager LocalConfig; - - protected SessionStatics SessionStatics { get; private set; } - - protected BeatmapManager BeatmapManager; - - protected ScoreManager ScoreManager; - - protected BeatmapDifficultyCache DifficultyCache; - - protected UserLookupCache UserCache; - - protected SkinManager SkinManager; - - protected RulesetStore RulesetStore; - - protected FileStore FileStore; - - protected KeyBindingStore KeyBindingStore; - - protected SettingsStore SettingsStore; - - protected RulesetConfigCache RulesetConfigCache; - - protected IAPIProvider API; - - private SpectatorClient spectatorClient; - private MultiplayerClient multiplayerClient; - - protected MenuCursorContainer MenuCursorContainer; - - protected MusicController MusicController; - - private Container content; - - protected override Container Content => content; - - protected Storage Storage { get; set; } - - [Cached] - [Cached(typeof(IBindable))] - protected readonly Bindable Ruleset = new Bindable(); - - /// - /// The current mod selection for the local user. - /// - /// - /// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy. - /// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection. - /// As such, all settings should be finalised before adding a mod to this collection. - /// - [Cached] - [Cached(typeof(IBindable>))] - protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); - - /// - /// Mods available for the current . - /// - public readonly Bindable>> AvailableMods = new Bindable>>(); - - protected Bindable Beatmap { get; private set; } // cached via load() method - - private Bindable fpsDisplayVisible; - public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); /// @@ -144,30 +83,83 @@ namespace osu.Game } } + protected OsuConfigManager LocalConfig { get; private set; } + + protected SessionStatics SessionStatics { get; private set; } + + protected BeatmapManager BeatmapManager { get; private set; } + + protected ScoreManager ScoreManager { get; private set; } + + protected SkinManager SkinManager { get; private set; } + + protected RulesetStore RulesetStore { get; private set; } + + protected KeyBindingStore KeyBindingStore { get; private set; } + + protected MenuCursorContainer MenuCursorContainer { get; private set; } + + protected MusicController MusicController { get; private set; } + + protected IAPIProvider API { get; set; } + + protected Storage Storage { get; set; } + + protected Bindable Beatmap { get; private set; } // cached via load() method + + [Cached] + [Cached(typeof(IBindable))] + protected readonly Bindable Ruleset = new Bindable(); + + /// + /// The current mod selection for the local user. + /// + /// + /// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy. + /// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection. + /// As such, all settings should be finalised before adding a mod to this collection. + /// + [Cached] + [Cached(typeof(IBindable>))] + protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); + + /// + /// Mods available for the current . + /// + public readonly Bindable>> AvailableMods = new Bindable>>(); + + private BeatmapDifficultyCache difficultyCache; + + private UserLookupCache userCache; + + private FileStore fileStore; + + private SettingsStore settingsStore; + + private RulesetConfigCache rulesetConfigCache; + + private SpectatorClient spectatorClient; + + private MultiplayerClient multiplayerClient; + + private DatabaseContextFactory contextFactory; + + protected override Container Content => content; + + private Container content; + + private DependencyContainer dependencies; + + private Bindable fpsDisplayVisible; + + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); + public OsuGameBase() { UseDevelopmentServer = DebugUtils.IsDebugBuild; Name = @"osu!lazer"; } - private DependencyContainer dependencies; - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => - dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - - private DatabaseContextFactory contextFactory; - - protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); - - protected virtual BatteryInfo CreateBatteryInfo() => null; - - /// - /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. - /// - internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5; - - private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); - [BackgroundDependencyLoader] private void load() { @@ -246,10 +238,10 @@ namespace osu.Game var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); dependencies.Cache(RulesetStore = new RulesetStore(contextFactory, Storage)); - dependencies.Cache(FileStore = new FileStore(contextFactory, Storage)); + dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() - dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => DifficultyCache, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap, true)); // this should likely be moved to ArchiveModelManager when another case appers where it is necessary @@ -273,19 +265,19 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); - dependencies.Cache(DifficultyCache = new BeatmapDifficultyCache()); - AddInternal(DifficultyCache); + dependencies.Cache(difficultyCache = new BeatmapDifficultyCache()); + AddInternal(difficultyCache); - dependencies.Cache(UserCache = new UserLookupCache()); - AddInternal(UserCache); + dependencies.Cache(userCache = new UserLookupCache()); + AddInternal(userCache); var scorePerformanceManager = new ScorePerformanceCache(); dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); - dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); + dependencies.Cache(settingsStore = new SettingsStore(contextFactory)); + dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore)); var powerStatus = CreateBatteryInfo(); if (powerStatus != null) @@ -308,7 +300,7 @@ namespace osu.Game dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); - FileStore.Cleanup(); + fileStore.Cleanup(); // add api components to hierarchy. if (API is APIAccess apiAccess) @@ -316,7 +308,7 @@ namespace osu.Game AddInternal(spectatorClient); AddInternal(multiplayerClient); - AddInternal(RulesetConfigCache); + AddInternal(rulesetConfigCache); GlobalActionContainer globalBindings; @@ -344,24 +336,6 @@ namespace osu.Game Ruleset.BindValueChanged(onRulesetChanged); } - private void onRulesetChanged(ValueChangedEvent r) - { - var dict = new Dictionary>(); - - if (r.NewValue?.Available == true) - { - foreach (ModType type in Enum.GetValues(typeof(ModType))) - dict[type] = r.NewValue.CreateInstance().GetModsFor(type).ToList(); - } - - if (!SelectedMods.Disabled) - SelectedMods.Value = Array.Empty(); - - AvailableMods.Value = dict; - } - - protected virtual Container CreateScalingContainer() => new DrawSizePreservingFillContainer(); - protected override void LoadComplete() { base.LoadComplete(); @@ -375,28 +349,8 @@ namespace osu.Game FrameStatistics.ValueChanged += e => fpsDisplayVisible.Value = e.NewValue != FrameStatisticsMode.None; } - private void runMigrations() - { - try - { - using (var db = contextFactory.GetForWrite(false)) - db.Context.Migrate(); - } - catch (Exception e) - { - Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database); - - // if we failed, let's delete the database and start fresh. - // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. - contextFactory.ResetDatabase(); - - Logger.Log("Database purged successfully.", LoggingTarget.Database); - - // only run once more, then hard bail. - using (var db = contextFactory.GetForWrite(false)) - db.Context.Migrate(); - } - } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); public override void SetHost(GameHost host) { @@ -422,51 +376,59 @@ namespace osu.Game Scheduler.AddDelayed(GracefullyExit, 2000); } + public void Migrate(string path) + { + contextFactory.FlushConnections(); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); + } + + protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); + + protected virtual BatteryInfo CreateBatteryInfo() => null; + + protected virtual Container CreateScalingContainer() => new DrawSizePreservingFillContainer(); + protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); - private readonly List fileImporters = new List(); - - /// - /// Register a global handler for file imports. Most recently registered will have precedence. - /// - /// The handler to register. - public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler); - - /// - /// Unregister a global handler for file imports. - /// - /// The previously registered handler. - public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler); - - public async Task Import(params string[] paths) + private void onRulesetChanged(ValueChangedEvent r) { - if (paths.Length == 0) - return; + var dict = new Dictionary>(); - var filesPerExtension = paths.GroupBy(p => Path.GetExtension(p).ToLowerInvariant()); - - foreach (var groups in filesPerExtension) + if (r.NewValue?.Available == true) { - foreach (var importer in fileImporters) - { - if (importer.HandledExtensions.Contains(groups.Key)) - await importer.Import(groups.ToArray()).ConfigureAwait(false); - } + foreach (ModType type in Enum.GetValues(typeof(ModType))) + dict[type] = r.NewValue.CreateInstance().GetModsFor(type).ToList(); + } + + if (!SelectedMods.Disabled) + SelectedMods.Value = Array.Empty(); + + AvailableMods.Value = dict; + } + + private void runMigrations() + { + try + { + using (var db = contextFactory.GetForWrite(false)) + db.Context.Migrate(); + } + catch (Exception e) + { + Logger.Error(e.InnerException ?? e, "Migration failed! We'll be starting with a fresh database.", LoggingTarget.Database); + + // if we failed, let's delete the database and start fresh. + // todo: we probably want a better (non-destructive) migrations/recovery process at a later point than this. + contextFactory.ResetDatabase(); + + Logger.Log("Database purged successfully.", LoggingTarget.Database); + + // only run once more, then hard bail. + using (var db = contextFactory.GetForWrite(false)) + db.Context.Migrate(); } } - public virtual async Task Import(params ImportTask[] tasks) - { - var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant()); - await Task.WhenAll(tasksPerExtension.Select(taskGroup => - { - var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key)); - return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask; - })).ConfigureAwait(false); - } - - public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -477,37 +439,5 @@ namespace osu.Game contextFactory.FlushConnections(); } - - private class OsuUserInputManager : UserInputManager - { - protected override MouseButtonEventManager CreateButtonEventManagerFor(MouseButton button) - { - switch (button) - { - case MouseButton.Right: - return new RightMouseManager(button); - } - - return base.CreateButtonEventManagerFor(button); - } - - private class RightMouseManager : MouseButtonEventManager - { - public RightMouseManager(MouseButton button) - : base(button) - { - } - - public override bool EnableDrag => true; // allow right-mouse dragging for absolute scroll in scroll containers. - public override bool EnableClick => false; - public override bool ChangeFocusOnClick => false; - } - } - - public void Migrate(string path) - { - contextFactory.FlushConnections(); - (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); - } } } diff --git a/osu.Game/OsuGameBase_Importing.cs b/osu.Game/OsuGameBase_Importing.cs new file mode 100644 index 0000000000..d712da3553 --- /dev/null +++ b/osu.Game/OsuGameBase_Importing.cs @@ -0,0 +1,57 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using osu.Game.Database; + +namespace osu.Game +{ + public partial class OsuGameBase + { + private readonly List fileImporters = new List(); + + /// + /// Register a global handler for file imports. Most recently registered will have precedence. + /// + /// The handler to register. + public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler); + + /// + /// Unregister a global handler for file imports. + /// + /// The previously registered handler. + public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler); + + public async Task Import(params string[] paths) + { + if (paths.Length == 0) + return; + + var filesPerExtension = paths.GroupBy(p => Path.GetExtension(p).ToLowerInvariant()); + + foreach (var groups in filesPerExtension) + { + foreach (var importer in fileImporters) + { + if (importer.HandledExtensions.Contains(groups.Key)) + await importer.Import(groups.ToArray()).ConfigureAwait(false); + } + } + } + + public virtual async Task Import(params ImportTask[] tasks) + { + var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant()); + await Task.WhenAll(tasksPerExtension.Select(taskGroup => + { + var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key)); + return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask; + })).ConfigureAwait(false); + } + + public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); + } +} diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index cf62226d94..f654fa91cf 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -35,7 +35,11 @@ namespace osu.Game.Rulesets.Objects HitObject = hitObject; startTimeBindable.BindTo(HitObject.StartTimeBindable); - startTimeBindable.BindValueChanged(onStartTimeChanged, true); + startTimeBindable.BindValueChanged(_ => setInitialLifetime(), true); + + // Subscribe to this event before the DrawableHitObject so that the local callback is invoked before the entry is re-applied as a result of DefaultsApplied. + // This way, the DrawableHitObject can use OnApply() to overwrite the LifetimeStart that was set inside setInitialLifetime(). + HitObject.DefaultsApplied += _ => setInitialLifetime(); } // The lifetime, as set by the hitobject. @@ -88,8 +92,8 @@ namespace osu.Game.Rulesets.Objects protected virtual double InitialLifetimeOffset => 10000; /// - /// Resets according to the change in start time of the . + /// Set using . /// - private void onStartTimeChanged(ValueChangedEvent startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + private void setInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; } } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index ca331088ce..00cec80dcd 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -50,12 +50,17 @@ namespace osu.Game.Tests.Visual { base.SetUpSteps(); - AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); + AddStep("load editor", LoadEditor); AddUntilStep("wait for editor to load", () => EditorComponentsReady); AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType().Single()); AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } + protected virtual void LoadEditor() + { + LoadScreen(Editor = CreateEditor()); + } + /// /// Creates the ruleset for providing a corresponding beatmap to load the editor on. ///