diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index f18d982bc0..28ff4b4cdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableRepeatPoint : DrawableOsuHitObject + public class DrawableRepeatPoint : DrawableOsuHitObject, ITrackSnaking { private readonly RepeatPoint repeatPoint; private readonly DrawableSlider drawableSlider; @@ -71,5 +71,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } } + + public void UpdateSnakingPosition(Vector2 start, Vector2 end) => Position = repeatPoint.RepeatIndex % 2 == 1 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 6aa3268e5e..eb499b5da6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly DrawableHitCircle InitialCircle; - private readonly List components = new List(); + private readonly List components = new List(); private readonly Container ticks; private readonly Container repeatPoints; @@ -101,6 +101,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; repeatPoints.Add(drawableRepeatPoint); + components.Add(drawableRepeatPoint); AddNested(drawableRepeatPoint); } } @@ -126,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!InitialCircle.Judgements.Any(j => j.IsHit)) InitialCircle.Position = slider.Curve.PositionAt(progress); - foreach (var c in components) c.UpdateProgress(progress, repeat); + foreach (var c in components.OfType()) c.UpdateProgress(progress, repeat); + foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var t in ticks.Children) t.Tracking = Ball.Tracking; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs new file mode 100644 index 0000000000..b5fd87f60b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/ITrackSnaking.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + /// + /// A component which tracks the current end snaking position of a slider. + /// + public interface ITrackSnaking + { + void UpdateSnakingPosition(Vector2 start, Vector2 end); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 40ec57d434..2da285a434 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -61,6 +61,7 @@ namespace osu.Game.Rulesets.Osu.Objects public int RepeatCount { get; set; } = 1; private int stackHeight; + public override int StackHeight { get { return stackHeight; } @@ -130,6 +131,17 @@ namespace osu.Game.Rulesets.Osu.Objects var distanceProgress = d / length; var timeProgress = reversed ? 1 - distanceProgress : distanceProgress; + var firstSample = Samples.FirstOrDefault(s => s.Name == SampleInfo.HIT_NORMAL) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) + var sampleList = new List(); + + if (firstSample != null) + sampleList.Add(new SampleInfo + { + Bank = firstSample.Bank, + Volume = firstSample.Volume, + Name = @"slidertick", + }); + AddNested(new SliderTick { RepeatIndex = repeat, @@ -138,12 +150,7 @@ namespace osu.Game.Rulesets.Osu.Objects StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, - Samples = new List(Samples.Select(s => new SampleInfo - { - Bank = s.Bank, - Name = @"slidertick", - Volume = s.Volume - })) + Samples = sampleList }); } } diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 7d6001359a..a59d4607df 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -71,6 +71,7 @@ + diff --git a/osu.Game.Tests/Visual/TestCaseAutoplay.cs b/osu.Game.Tests/Visual/TestCaseAutoplay.cs new file mode 100644 index 0000000000..d954d0543c --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseAutoplay.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.Visual +{ + [Description("Player instantiated with an autoplay mod.")] + public class TestCaseAutoplay : TestCasePlayer + { + protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) + { + beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); + return base.CreatePlayer(beatmap, ruleset); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index 451c4e013f..237687458d 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -1,19 +1,35 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.ComponentModel; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [Description("Player instantiated with a replay.")] public class TestCaseReplay : TestCasePlayer { protected override Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) { + // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here + // to simulate setting a replay rather than having the replay already set for us beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - return base.CreatePlayer(beatmap, ruleset); + var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, false); + + // We have the replay + var replay = dummyRulesetContainer.Replay; + + // Reset the mods + beatmap.Mods.Value = beatmap.Mods.Value.Where(m => !(m is ModAutoplay)); + + return new ReplayPlayer(replay) + { + InitialBeatmap = beatmap + }; } } } diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 76b426bf5d..059adc6d55 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -137,6 +137,7 @@ + diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 02564489ad..df71c5c0d0 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -156,6 +156,7 @@ namespace osu.Game.Beatmaps public IQueryable Beatmaps => GetContext().BeatmapInfo .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) .Include(b => b.Metadata) .Include(b => b.Ruleset) .Include(b => b.BaseDifficulty); diff --git a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/PreviewButton.cs index 11d1769f1e..4f5a6ba718 100644 --- a/osu.Game/Overlays/BeatmapSet/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/PreviewButton.cs @@ -82,7 +82,8 @@ namespace osu.Game.Overlays.BeatmapSet if (Playing.Value && preview != null) { - progress.Width = (float)(preview.CurrentTime / preview.Length); + // prevent negative (potential infinite) width if a track without length was loaded + progress.Width = preview.Length > 0 ? (float)(preview.CurrentTime / preview.Length) : 0f; } } diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 9ecddb01ba..c00fb9b122 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; +using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -39,6 +40,8 @@ namespace osu.Game.Overlays.Direct private readonly SpriteIcon icon; private readonly LoadingAnimation loadingAnimation; + private readonly BindableDouble muteBindable = new BindableDouble(); + private const float transition_duration = 500; private bool loading @@ -83,9 +86,10 @@ namespace osu.Game.Overlays.Direct } [BackgroundDependencyLoader] - private void load(OsuColour colour) + private void load(OsuColour colour, AudioManager audio) { hoverColour = colour.Yellow; + this.audio = audio; } protected override bool OnClick(InputState state) @@ -128,21 +132,30 @@ namespace osu.Game.Overlays.Direct return; } - Preview.Seek(0); - Preview.Start(); + Preview.Restart(); + + audio.Track.AddAdjustment(AdjustableProperty.Volume, muteBindable); } else { + audio.Track.RemoveAdjustment(AdjustableProperty.Volume, muteBindable); + Preview?.Stop(); loading = false; } } private TrackLoader trackLoader; + private AudioManager audio; private void beginAudioLoad() { - if (trackLoader != null) return; + if (trackLoader != null) + { + Preview = trackLoader.Preview; + Playing.TriggerChange(); + return; + } loading = true; @@ -164,6 +177,7 @@ namespace osu.Game.Overlays.Direct private readonly string preview; public Track Preview; + private TrackManager trackManager; public TrackLoader(string preview) { @@ -171,10 +185,22 @@ namespace osu.Game.Overlays.Direct } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, FrameworkConfigManager config) { + // create a local trackManager to bypass the mute we are applying above. + audio.AddItem(trackManager = new TrackManager(new OnlineStore())); + + // add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy). + config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); + if (!string.IsNullOrEmpty(preview)) - Preview = audio.Track.Get(preview); + Preview = trackManager.Get(preview); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + trackManager?.Dispose(); } } } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 979a357d1e..d5acb9dc86 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -313,6 +313,14 @@ namespace osu.Game.Overlays api.Queue(getSetsRequest); } + protected override void PopOut() + { + base.PopOut(); + + if (playing != null) + playing.PreviewPlaying.Value = false; + } + private int distinctCount(List list) => list.Distinct().ToArray().Length; public class ResultCounts diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index fe366f52a5..d21ca79736 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -10,6 +10,7 @@ using osu.Game.Database; using osu.Game.IO.Legacy; using osu.Game.IPC; using osu.Game.Rulesets.Replays; +using osu.Game.Users; using SharpCompress.Compressors.LZMA; namespace osu.Game.Rulesets.Scoring @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Scoring var beatmapHash = sr.ReadString(); score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); /* score.PlayerName = */ - sr.ReadString(); + score.User = new User { Username = sr.ReadString() }; /* var localScoreChecksum = */ sr.ReadString(); /* score.Count300 = */ @@ -107,7 +108,10 @@ namespace osu.Game.Rulesets.Scoring using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize)) using (var reader = new StreamReader(lzma)) + { score.Replay = createLegacyReplay(reader); + score.Replay.User = score.User; + } } } @@ -129,9 +133,22 @@ namespace osu.Game.Rulesets.Scoring { var split = l.Split('|'); - if (split.Length < 4 || float.Parse(split[0]) < 0) continue; + if (split.Length < 4) + continue; - lastTime += float.Parse(split[0]); + if (split[0] == "-12345") + { + // Todo: The seed is provided in split[3], which we'll need to use at some point + continue; + } + + var diff = float.Parse(split[0]); + lastTime += diff; + + // Todo: At some point we probably want to rewind and play back the negative-time frames + // but for now we'll achieve equal playback to stable by skipping negative frames + if (diff < 0) + continue; frames.Add(new ReplayFrame( lastTime, diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 626b56ad67..bb4466208b 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -13,6 +13,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Configuration; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using osu.Game.Rulesets.Replays; @@ -45,9 +46,9 @@ namespace osu.Game.Rulesets.UI public PassThroughInputManager KeyBindingInputManager; /// - /// Whether we have a replay loaded currently. + /// Whether a replay is currently loaded. /// - public bool HasReplayLoaded => ReplayInputManager?.ReplayInputHandler != null; + public readonly BindableBool HasReplayLoaded = new BindableBool(); public abstract IEnumerable Objects { get; } @@ -99,6 +100,8 @@ namespace osu.Game.Rulesets.UI Replay = replay; ReplayInputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null; + + HasReplayLoaded.Value = ReplayInputManager.ReplayInputHandler != null; } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 721b5344ff..255c071ac1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play public readonly ReplaySettingsOverlay ReplaySettingsOverlay; private Bindable showHud; - private bool replayLoaded; + private readonly BindableBool replayLoaded = new BindableBool(); private static bool hasShownNotificationOnce; @@ -91,22 +91,39 @@ namespace osu.Game.Screens.Play } } - public virtual void BindRulesetContainer(RulesetContainer rulesetContainer) + protected override void LoadComplete() { - (rulesetContainer.KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(KeyCounter); + base.LoadComplete(); - replayLoaded = rulesetContainer.HasReplayLoaded; + replayLoaded.ValueChanged += replayLoadedValueChanged; + replayLoaded.TriggerChange(); + } - ReplaySettingsOverlay.ReplayLoaded = replayLoaded; + private void replayLoadedValueChanged(bool loaded) + { + ReplaySettingsOverlay.ReplayLoaded = loaded; - // in the case a replay isn't loaded, we want some elements to only appear briefly. - if (!replayLoaded) + if (loaded) + { + ReplaySettingsOverlay.Show(); + ModDisplay.FadeIn(200); + } + else { ReplaySettingsOverlay.Hide(); ModDisplay.Delay(2000).FadeOut(200); } } + public virtual void BindRulesetContainer(RulesetContainer rulesetContainer) + { + (rulesetContainer.KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(KeyCounter); + + replayLoaded.BindTo(rulesetContainer.HasReplayLoaded); + + Progress.BindRulestContainer(rulesetContainer); + } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (args.Repeat) return false; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 31d9fac2ad..8d26d63d41 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play public int RestartCount; public CursorContainer Cursor => RulesetContainer.Cursor; - public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded; + public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value; private IAdjustableClock adjustableSourceClock; private FramedOffsetClock offsetClock; @@ -226,7 +226,6 @@ namespace osu.Game.Screens.Play hudOverlay.Progress.Objects = RulesetContainer.Objects; hudOverlay.Progress.AudioClock = decoupledClock; - hudOverlay.Progress.AllowSeeking = RulesetContainer.HasReplayLoaded; hudOverlay.Progress.OnSeek = pos => decoupledClock.Seek(pos); hudOverlay.ModDisplay.Current.BindTo(working.Mods); diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index 8aad741ad1..12f501a632 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -9,9 +9,12 @@ using System.Collections.Generic; using osu.Game.Graphics; using osu.Framework.Allocation; using System.Linq; +using osu.Framework.Configuration; using osu.Framework.Timing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.UI; + namespace osu.Game.Screens.Play { public class SongProgress : OverlayContainer @@ -54,6 +57,8 @@ namespace osu.Game.Screens.Play } } + private readonly BindableBool replayLoaded = new BindableBool(); + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -98,6 +103,14 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { State = Visibility.Visible; + + replayLoaded.ValueChanged += v => AllowSeeking = v; + replayLoaded.TriggerChange(); + } + + public void BindRulestContainer(RulesetContainer rulesetContainer) + { + replayLoaded.BindTo(rulesetContainer.HasReplayLoaded); } private bool allowSeeking;