diff --git a/appveyor.yml b/appveyor.yml index c25c2a659e..15484e4c68 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,3 @@ -# 2017-09-14 clone_depth: 1 version: '{branch}-{build}' image: Visual Studio 2017 diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml index dc43a9cb13..cadebd9d11 100644 --- a/appveyor_deploy.yml +++ b/appveyor_deploy.yml @@ -1,4 +1,8 @@ -# 2017-09-14 +branches: + only: + - release +skip_tags: true +skip_branch_with_pr: true clone_depth: 1 version: '{branch}-{build}' image: Visual Studio 2017 diff --git a/osu-framework b/osu-framework index a5e5b64a92..ac2b966644 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit a5e5b64a9270df6704e7d78126e7b1541064f209 +Subproject commit ac2b966644982a35ba96d315a7bb5367e0bedb99 diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 8c460f03cf..16bbf90cd4 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -7,6 +7,7 @@ using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; +using System.Management.Automation; using Newtonsoft.Json; using osu.Framework.IO.Network; using FileWebRequest = osu.Framework.IO.Network.FileWebRequest; @@ -99,6 +100,7 @@ namespace osu.Desktop.Deploy write("Updating AssemblyInfo..."); updateCsprojVersion(version); + updateAppveyorVersion(version); write("Running build process..."); foreach (string targetName in TargetNames.Split(',')) @@ -404,6 +406,25 @@ namespace osu.Desktop.Deploy Console.WriteLine(); } + private static bool updateAppveyorVersion(string version) + { + try + { + using (PowerShell ps = PowerShell.Create()) + { + ps.AddScript($"Update-AppveyorBuild -Version \"{version}\""); + ps.Invoke(); + } + return true; + } + catch + { + // we don't have appveyor and don't care + } + + return false; + } + private static void write(string message, ConsoleColor col = ConsoleColor.Gray) { if (sw.ElapsedMilliseconds > 0) diff --git a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj index d3f6a4aed5..a97b8197b4 100644 --- a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj +++ b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj @@ -1,4 +1,4 @@ - + net471 @@ -14,5 +14,6 @@ + \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs index 3518e030b6..6213bb1329 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs @@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuEditPlayfield : OsuPlayfield { - protected override bool ProxyApproachCircles => false; protected override bool DisplayJudgements => false; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f9d21ea5bb..10539f85a2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Children = new Drawable[] + Children = new[] { Background = new SpinnerBackground { diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 8330aa8e9d..b63f85623e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -21,9 +21,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly JudgementContainer judgementLayer; private readonly ConnectionRenderer connectionLayer; - // Todo: This should not be a thing, but is currently required for the editor - // https://github.com/ppy/osu-framework/issues/1283 - protected virtual bool ProxyApproachCircles => true; protected virtual bool DisplayJudgements => true; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -59,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.UI h.OnJudgement += onJudgement; var c = h as IDrawableHitObjectWithProxiedApproach; - if (c != null && ProxyApproachCircles) + if (c != null) approachCircles.Add(c.ProxiedLayer.CreateProxy()); base.Add(h); diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index c39e9abf8b..157c814f55 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -71,9 +71,9 @@ namespace osu.Game.Graphics.Containers switch (linkType) { case LinkAction.OpenBeatmap: - // todo: replace this with overlay.ShowBeatmap(id) once an appropriate API call is implemented. - if (int.TryParse(linkArgument, out int beatmapId)) - Process.Start($"https://osu.ppy.sh/b/{beatmapId}"); + // TODO: proper query params handling + if (linkArgument != null && int.TryParse(linkArgument.Contains('?') ? linkArgument.Split('?')[0] : linkArgument, out int beatmapId)) + game?.ShowBeatmap(beatmapId); break; case LinkAction.OpenBeatmapSet: if (int.TryParse(linkArgument, out int setId)) diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index b031791900..37dd77af46 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -5,13 +5,21 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapSetRequest : APIRequest { - private readonly int beatmapSetId; + private readonly int id; + private readonly BeatmapSetLookupType type; - public GetBeatmapSetRequest(int beatmapSetId) + public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId) { - this.beatmapSetId = beatmapSetId; + this.id = id; + this.type = type; } - protected override string Target => $@"beatmapsets/{beatmapSetId}"; + protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}"; + } + + public enum BeatmapSetLookupType + { + SetId, + BeatmapId, } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 578d88bd89..3852580c49 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -166,6 +166,12 @@ namespace osu.Game /// The user to display. public void ShowUser(long userId) => userProfile.ShowUser(userId); + /// + /// Show a beatmap's set as an overlay, displaying the given beatmap. + /// + /// The beatmap to show. + public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); + protected void LoadScore(Score s) { scoreLoad?.Cancel(); diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 366c34eae3..096f7bb63c 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -17,21 +17,39 @@ using osu.Game.Online.API.Requests; using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; using osu.Game.Overlays.BeatmapSet.Scores; +using System.Linq; namespace osu.Game.Overlays { public class BeatmapSetOverlay : WaveOverlayContainer { + private const int fade_duration = 300; + public const float X_PADDING = 40; public const float RIGHT_WIDTH = 275; private readonly Header header; private readonly Info info; + private APIAccess api; private RulesetStore rulesets; private readonly ScrollContainer scroll; + private BeatmapSetInfo beatmapSet; + + public BeatmapSetInfo BeatmapSet + { + get => beatmapSet; + set + { + if (value == beatmapSet) + return; + + header.BeatmapSet = info.BeatmapSet = beatmapSet = value; + } + } + // receive input outside our bounds so we can trigger a close event on ourselves. public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; @@ -107,7 +125,8 @@ namespace osu.Game.Overlays { base.PopOut(); header.Details.StopPreview(); - FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out); + + FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.Out).OnComplete(_ => BeatmapSet = null); } protected override bool OnClick(InputState state) @@ -116,17 +135,31 @@ namespace osu.Game.Overlays return true; } + public void FetchAndShowBeatmap(int beatmapId) + { + BeatmapSet = null; + var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + req.Success += res => + { + ShowBeatmapSet(res.ToBeatmapSet(rulesets)); + header.Picker.Beatmap.Value = header.BeatmapSet.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); + }; + api.Queue(req); + Show(); + } + public void FetchAndShowBeatmapSet(int beatmapSetId) { - // todo: display the overlay while we are loading here. we need to support setting BeatmapSet to null for this to work. + BeatmapSet = null; var req = new GetBeatmapSetRequest(beatmapSetId); req.Success += res => ShowBeatmapSet(res.ToBeatmapSet(rulesets)); api.Queue(req); + Show(); } public void ShowBeatmapSet(BeatmapSetInfo set) { - header.BeatmapSet = info.BeatmapSet = set; + BeatmapSet = set; Show(); scroll.ScrollTo(0); } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index edfea57e94..36c6b07f54 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.UI.Scrolling base.UpdateAfterChildrenLife(); // We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions - speedChangeVisualiser.ComputePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize); + speedChangeVisualiser.UpdatePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize); } } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs index 02791e0517..097e28b2dc 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/ISpeedChangeVisualiser.cs @@ -10,24 +10,23 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers public interface ISpeedChangeVisualiser { /// - /// Computes the states of s that are constant, such as lifetime and spatial length. + /// Computes the states of s that remain constant while scrolling, such as lifetime and spatial length. /// This is invoked once whenever or changes. /// /// The s whose states should be computed. /// The scrolling direction. - /// The duration required to scroll through one length of the screen before any control point adjustments. + /// The duration required to scroll through one length of the screen before any speed adjustments. /// The length of the screen that is scrolled through. void ComputeInitialStates(IEnumerable hitObjects, ScrollingDirection direction, double timeRange, Vector2 length); /// - /// Computes the states of s that change depending on , such as position. - /// This is invoked once per frame. + /// Updates the positions of s, depending on the current time. This is invoked once per frame. /// - /// The s whose states should be computed. + /// The s whose positions should be computed. /// The scrolling direction. /// The current time. - /// The duration required to scroll through one length of the screen before any control point adjustments. + /// The duration required to scroll through one length of the screen before any speed adjustments. /// The length of the screen that is scrolled through. - void ComputePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length); + void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length); } } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs index 2365582645..35a91275a7 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Lists; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Timing; using OpenTK; @@ -22,24 +23,45 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers { foreach (var obj in hitObjects) { - var controlPoint = controlPointAt(obj.HitObject.StartTime); - obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier; + // The total amount of time that the hitobject will remain visible within the timeRange, which decreases as the speed multiplier increases + double visibleDuration = timeRange / controlPointAt(obj.HitObject.StartTime).Multiplier; + + obj.LifetimeStart = obj.HitObject.StartTime - visibleDuration; + + if (obj.HitObject is IHasEndTime endTime) + { + // At the hitobject's end time, the hitobject will be positioned such that its end rests at the origin. + // This results in a negative-position value, and the absolute of it indicates the length of the hitobject. + var hitObjectLength = -hitObjectPositionAt(obj, endTime.EndTime, timeRange); + + switch (direction) + { + case ScrollingDirection.Up: + case ScrollingDirection.Down: + obj.Height = (float)(hitObjectLength * length.Y); + break; + case ScrollingDirection.Left: + case ScrollingDirection.Right: + obj.Width = (float)(hitObjectLength * length.X); + break; + } + } if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); + + // Nested hitobjects don't need to scroll, but they do need accurate positions + UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); } } } - public void ComputePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) + public void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) { foreach (var obj in hitObjects) { - var controlPoint = controlPointAt(obj.HitObject.StartTime); - - var position = (obj.HitObject.StartTime - currentTime) * controlPoint.Multiplier / timeRange; + var position = hitObjectPositionAt(obj, currentTime, timeRange); switch (direction) { @@ -59,7 +81,28 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } + /// + /// Computes the position of a at a point in time. + /// + /// At t < startTime, position > 0.
+ /// At t = startTime, position = 0.
+ /// At t > startTime, position < 0. + ///
+ ///
+ /// The . + /// The time to find the position of at. + /// The amount of time visualised by the scrolling area. + /// The position of in the scrolling area at time = . + private double hitObjectPositionAt(DrawableHitObject obj, double time, double timeRange) + => (obj.HitObject.StartTime - time) / timeRange * controlPointAt(obj.HitObject.StartTime).Multiplier; + private readonly MultiplierControlPoint searchPoint = new MultiplierControlPoint(); + + /// + /// Finds the which affects the speed of hitobjects at a specific time. + /// + /// The time which the should affect. + /// The . private MultiplierControlPoint controlPointAt(double time) { if (controlPoints.Count == 0) diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs index 708a2f173b..e353c07e9f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs @@ -25,23 +25,25 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers { foreach (var obj in hitObjects) { + // To reduce iterations when updating hitobject positions later on, their initial positions are cached var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime, timeRange); + // Todo: This is approximate and will be incorrect in the case of extreme speed changes obj.LifetimeStart = obj.HitObject.StartTime - timeRange - 1000; if (obj.HitObject is IHasEndTime endTime) { - var diff = positionAt(endTime.EndTime, timeRange) - startPosition; + var hitObjectLength = positionAt(endTime.EndTime, timeRange) - startPosition; switch (direction) { case ScrollingDirection.Up: case ScrollingDirection.Down: - obj.Height = (float)(diff * length.Y); + obj.Height = (float)(hitObjectLength * length.Y); break; case ScrollingDirection.Left: case ScrollingDirection.Right: - obj.Width = (float)(diff * length.X); + obj.Width = (float)(hitObjectLength * length.X); break; } } @@ -49,12 +51,14 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); - ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); + + // Nested hitobjects don't need to scroll, but they do need accurate positions + UpdatePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); } } } - public void ComputePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) + public void UpdatePositions(IEnumerable hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length) { var timelinePosition = positionAt(currentTime, timeRange); @@ -80,21 +84,38 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } + /// + /// Finds the position which corresponds to a point in time. + /// This is a non-linear operation that depends on all the control points up to and including the one active at the time value. + /// + /// The time to find the position at. + /// The amount of time visualised by the scrolling area. + /// A positive value indicating the position at . private double positionAt(double time, double timeRange) { double length = 0; + + // We need to consider all timing points until the specified time and not just the currently-active one, + // since each timing point individually affects the positions of _all_ hitobjects after its start time for (int i = 0; i < controlPoints.Count; i++) { var current = controlPoints[i]; var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; + // We don't need to consider any control points beyond the current time, since it will not yet + // affect any hitobjects if (i > 0 && current.StartTime > time) continue; // Duration of the current control point var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime; - length += Math.Min(currentDuration, time - current.StartTime) * current.Multiplier / timeRange; + // We want to consider the minimal amount of time that this control point has affected, + // which may be either its duration, or the amount of time that has passed within it + var durationInCurrent = Math.Min(currentDuration, time - current.StartTime); + + // Figure out how much of the time range the duration represents, and adjust it by the speed multiplier + length += durationInCurrent / timeRange * current.Multiplier; } return length; diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs index ede98d986a..b9a8e9914a 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Edit.Screens.Compose.Layers var dragLayer = new DragLayer(maskContainer.Select); dragLayer.DragEnd += () => maskSelection.UpdateVisibility(); - InternalChildren = new Drawable[] + InternalChildren = new[] { dragLayer, maskSelection, diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index cda8a549da..f39952dc31 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using OpenTK; using OpenTK.Graphics; @@ -35,6 +36,8 @@ namespace osu.Game.Screens.Select.Carousel private Triangles triangles; private StarCounter starCounter; + private BeatmapSetOverlay beatmapOverlay; + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { beatmap = panel.Beatmap; @@ -42,8 +45,10 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(SongSelect songSelect, BeatmapManager manager) + private void load(SongSelect songSelect, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) { + this.beatmapOverlay = beatmapOverlay; + if (songSelect != null) { startRequested = songSelect.FinaliseSelection; @@ -171,6 +176,10 @@ namespace osu.Game.Screens.Select.Carousel new OsuMenuItem("Play", MenuItemType.Highlighted, () => startRequested?.Invoke(beatmap)), new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested?.Invoke(beatmap)), new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested?.Invoke(beatmap)), + new OsuMenuItem("Details", MenuItemType.Standard, () => + { + if (beatmap.OnlineBeatmapID.HasValue) beatmapOverlay?.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value); + }), }; } }