1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 05:27:23 +08:00

Merge branch 'master' into applause-stops-on-score-switch

This commit is contained in:
Vinicius Barbosa 2021-05-19 09:52:31 +02:00 committed by GitHub
commit 4f397ae7f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1165 additions and 188 deletions

View File

@ -15,7 +15,6 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Tests.Editor
@ -35,7 +34,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceBeforeCurrentTimeDownwards()
{
AddStep("move mouse before current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single().ScreenSpaceDrawQuad.BottomLeft - new Vector2(0, 10)));
AddStep("move mouse before current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(-100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
@ -45,7 +48,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test]
public void TestPlaceAfterCurrentTimeDownwards()
{
AddStep("move mouse after current time", () => InputManager.MoveMouseTo(this.ChildrenOfType<Column>().Single()));
AddStep("move mouse after current time", () =>
{
var column = this.ChildrenOfType<Column>().Single();
InputManager.MoveMouseTo(column.ScreenSpacePositionAtTime(100));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

@ -0,0 +1,67 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneDrawableManiaHitObject : OsuTestScene
{
private readonly ManualClock clock = new ManualClock();
private Column column;
[SetUp]
public void SetUp() => Schedule(() =>
{
Child = new ScrollingTestContainer(ScrollingDirection.Down)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
TimeRange = 2000,
Clock = new FramedClock(clock),
Child = column = new Column(0)
{
Action = { Value = ManiaAction.Key1 },
Height = 0.85f,
AccentColour = Color4.Gray
},
};
});
[Test]
public void TestHoldNoteHeadVisibility()
{
DrawableHoldNote note = null;
AddStep("Add hold note", () =>
{
var h = new HoldNote
{
StartTime = 0,
Duration = 1000
};
h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
column.Add(note = new DrawableHoldNote(h));
});
AddStep("Hold key", () =>
{
clock.CurrentTime = 0;
note.OnPressed(ManiaAction.Key1);
});
AddStep("progress time", () => clock.CurrentTime = 500);
AddAssert("head is visible", () => note.Head.Alpha == 1);
}
}
}

View File

@ -4,14 +4,11 @@
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@ -411,17 +408,7 @@ namespace osu.Game.Rulesets.Mania.Tests
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head);
AddAssert("head is visible",
() => currentPlayer.ChildrenOfType<DrawableHoldNote>()
.Single(note => note.HitObject == beatmap.HitObjects[0])
.Head
.Alpha == 1);
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor?.HasCompleted.Value == true);
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer

View File

@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Edit
foreach (var line in grid.Objects.OfType<DrawableGridLine>())
availableLines.Push(line);
grid.Clear(false);
grid.Clear();
}
if (selectionTimeRange == null)

View File

@ -12,12 +12,23 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using Vector2 = osuTK.Vector2;
namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : EditorSelectionHandler
{
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
protected override void OnSelectionChanged()
{
base.OnSelectionChanged();
@ -50,17 +61,6 @@ namespace osu.Game.Rulesets.Osu.Edit
return true;
}
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
/// <summary>
/// During a transform, the initial path types of a single selected slider are stored so they
/// can be maintained throughout the operation.
/// </summary>
private List<PathType?> referencePathTypes;
public override bool HandleReverse()
{
var hitObjects = EditorBeatmap.SelectedHitObjects;
@ -114,24 +114,10 @@ namespace osu.Game.Rulesets.Osu.Edit
var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects)
{
var pos = h.Position;
switch (direction)
{
case Direction.Horizontal:
pos.X = centre.X - (pos.X - centre.X);
break;
case Direction.Vertical:
pos.Y = centre.Y - (pos.Y - centre.Y);
break;
}
h.Position = pos;
h.Position = GetFlippedPosition(direction, selectedObjectsQuad, h.Position);
if (h is Slider slider)
{
@ -204,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit
{
referencePathTypes ??= slider.Path.ControlPoints.Select(p => p.Type.Value).ToList();
Quad sliderQuad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
Quad sliderQuad = GetSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
// Limit minimum distance between control points after scaling to almost 0. Less than 0 causes the slider to flip, exactly 0 causes a crash through division by 0.
scale = Vector2.ComponentMax(new Vector2(Precision.FLOAT_EPSILON), sliderQuad.Size + scale) - sliderQuad.Size;
@ -333,7 +319,7 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
getSurroundingQuad(hitObjects.SelectMany(h =>
GetSurroundingQuad(hitObjects.SelectMany(h =>
{
if (h is IHasPath path)
{
@ -348,30 +334,6 @@ namespace osu.Game.Rulesets.Osu.Edit
return new[] { h.Position };
}));
/// <summary>
/// Returns a gamefield-space quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!EditorBeatmap.SelectedHitObjects.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[SetUpSteps]
public void SetUp()
=> AddStep("clear SHOC", () => hitObjectContainer.Clear(false));
=> AddStep("clear SHOC", () => hitObjectContainer.Clear());
protected void AddHitObject(DrawableHitObject hitObject)
=> AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject));

View File

@ -0,0 +1,175 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Editing
{
[HeadlessTest]
public class TestSceneHitObjectContainerEventBuffer : OsuTestScene
{
private readonly TestHitObject testObj = new TestHitObject();
private TestPlayfield playfield1;
private TestPlayfield playfield2;
private TestDrawable intermediateDrawable;
private HitObjectUsageEventBuffer eventBuffer;
private HitObject beganUsage;
private HitObject finishedUsage;
private HitObject transferredUsage;
[SetUp]
public void Setup() => Schedule(() =>
{
reset();
if (eventBuffer != null)
{
eventBuffer.HitObjectUsageBegan -= onHitObjectUsageBegan;
eventBuffer.HitObjectUsageFinished -= onHitObjectUsageFinished;
eventBuffer.HitObjectUsageTransferred -= onHitObjectUsageTransferred;
}
var topPlayfield = new TestPlayfield();
topPlayfield.AddNested(playfield1 = new TestPlayfield());
topPlayfield.AddNested(playfield2 = new TestPlayfield());
eventBuffer = new HitObjectUsageEventBuffer(topPlayfield);
eventBuffer.HitObjectUsageBegan += onHitObjectUsageBegan;
eventBuffer.HitObjectUsageFinished += onHitObjectUsageFinished;
eventBuffer.HitObjectUsageTransferred += onHitObjectUsageTransferred;
Children = new Drawable[]
{
topPlayfield,
intermediateDrawable = new TestDrawable(),
};
});
private void onHitObjectUsageBegan(HitObject obj) => beganUsage = obj;
private void onHitObjectUsageFinished(HitObject obj) => finishedUsage = obj;
private void onHitObjectUsageTransferred(HitObject obj, DrawableHitObject drawableObj) => transferredUsage = obj;
[Test]
public void TestUsageBeganAfterAdd()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addCheckStep(began: true);
}
[Test]
public void TestUsageFinishedAfterRemove()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("remove hitobject", () => playfield1.Remove(testObj));
addCheckStep(finished: true);
}
[Test]
public void TestUsageTransferredWhenMovedBetweenPlayfields()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("transfer hitobject to other playfield", () =>
{
playfield1.Remove(testObj);
playfield2.Add(testObj);
});
addCheckStep(transferred: true);
}
[Test]
public void TestRemoveImmediatelyAfterUsageBegan()
{
AddStep("add hitobject and schedule removal", () =>
{
playfield1.Add(testObj);
intermediateDrawable.Schedule(() => playfield1.Remove(testObj));
});
addCheckStep(began: true, finished: true);
}
[Test]
public void TestRemoveImmediatelyAfterTransferred()
{
AddStep("add hitobject", () => playfield1.Add(testObj));
addResetStep();
AddStep("transfer hitobject to other playfield and schedule removal", () =>
{
playfield1.Remove(testObj);
playfield2.Add(testObj);
intermediateDrawable.Schedule(() => playfield2.Remove(testObj));
});
addCheckStep(transferred: true, finished: true);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
eventBuffer.Update();
}
private void addResetStep() => AddStep("reset", reset);
private void reset()
{
beganUsage = null;
finishedUsage = null;
transferredUsage = null;
}
private void addCheckStep(bool began = false, bool finished = false, bool transferred = false)
=> AddAssert($"began = {began}, finished = {finished}, transferred = {transferred}",
() => (beganUsage == testObj) == began && (finishedUsage == testObj) == finished && (transferredUsage == testObj) == transferred);
private class TestPlayfield : Playfield
{
public TestPlayfield()
{
RegisterPool<TestHitObject, TestDrawableHitObject>(1);
}
public new void AddNested(Playfield playfield)
{
AddInternal(playfield);
base.AddNested(playfield);
}
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject)
{
var entry = base.CreateLifetimeEntry(hitObject);
entry.KeepAlive = true;
return entry;
}
}
private class TestHitObject : HitObject
{
public override string ToString() => "TestHitObject";
}
private class TestDrawableHitObject : DrawableHitObject
{
}
private class TestDrawable : Drawable
{
public new void Schedule(Action action) => base.Schedule(action);
}
}
}

View File

@ -45,15 +45,16 @@ namespace osu.Game.Tests.Gameplay
AddStep("Create DHO", () =>
{
dho = new TestDrawableHitObject(null);
dho.Apply(entry = new TestLifetimeEntry(new HitObject())
{
LifetimeStart = 0,
LifetimeEnd = 1000,
});
dho.Apply(entry = new TestLifetimeEntry(new HitObject()));
Child = dho;
});
AddStep("KeepAlive = true", () => entry.KeepAlive = true);
AddStep("KeepAlive = true", () =>
{
entry.LifetimeStart = 0;
entry.LifetimeEnd = 1000;
entry.KeepAlive = true;
});
AddAssert("Lifetime is overriden", () => entry.LifetimeStart == double.MinValue && entry.LifetimeEnd == double.MaxValue);
AddStep("Set LifetimeStart", () => dho.LifetimeStart = 500);

View File

@ -0,0 +1,152 @@
// 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.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.News.Sidebar;
using static osu.Game.Overlays.News.Sidebar.YearsPanel;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneNewsSidebar : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
private TestNewsSidebar sidebar;
[SetUp]
public void SetUp() => Schedule(() => Child = sidebar = new TestNewsSidebar { YearChanged = onYearChanged });
[Test]
public void TestBasic()
{
AddStep("Add metadata", () => sidebar.Metadata.Value = getMetadata(2021));
AddUntilStep("Month sections exist", () => sidebar.ChildrenOfType<MonthSection>().Any());
}
[Test]
public void TestMetadataWithNoPosts()
{
AddStep("Add data with no posts", () => sidebar.Metadata.Value = metadata_with_no_posts);
AddUntilStep("No month sections were created", () => !sidebar.ChildrenOfType<MonthSection>().Any());
}
[Test]
public void TestYearsPanelVisibility()
{
AddUntilStep("Years panel is hidden", () => yearsPanel?.Alpha == 0);
AddStep("Add data", () => sidebar.Metadata.Value = getMetadata(2021));
AddUntilStep("Years panel is visible", () => yearsPanel?.Alpha == 1);
}
private void onYearChanged(int year) => sidebar.Metadata.Value = getMetadata(year);
private YearsPanel yearsPanel => sidebar.ChildrenOfType<YearsPanel>().FirstOrDefault();
private APINewsSidebar getMetadata(int year) => new APINewsSidebar
{
CurrentYear = year,
Years = new[]
{
2021,
2020,
2019,
2018,
2017,
2016,
2015,
2014,
2013
},
NewsPosts = new List<APINewsPost>
{
new APINewsPost
{
Title = "(Mar) Short title",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Mar) Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Mar) Medium title, nothing to see here",
PublishedAt = new DateTime(year, 3, 1)
},
new APINewsPost
{
Title = "(Feb) Short title",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "(Feb) Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "(Feb) Medium title, nothing to see here",
PublishedAt = new DateTime(year, 2, 1)
},
new APINewsPost
{
Title = "Short title",
PublishedAt = new DateTime(year, 1, 1)
},
new APINewsPost
{
Title = "Oh boy that's a long post title I wonder if it will break anything",
PublishedAt = new DateTime(year, 1, 1)
},
new APINewsPost
{
Title = "Medium title, nothing to see here",
PublishedAt = new DateTime(year, 1, 1)
}
}
};
private static readonly APINewsSidebar metadata_with_no_posts = new APINewsSidebar
{
CurrentYear = 2021,
Years = new[]
{
2021,
2020,
2019,
2018,
2017,
2016,
2015,
2014,
2013
},
NewsPosts = Array.Empty<APINewsPost>()
};
private class TestNewsSidebar : NewsSidebar
{
public Action<int> YearChanged;
protected override void LoadComplete()
{
base.LoadComplete();
Metadata.BindValueChanged(metadata =>
{
foreach (var b in this.ChildrenOfType<YearButton>())
b.Action = () => YearChanged?.Invoke(b.Year);
}, true);
}
}
}
}

View File

@ -11,5 +11,8 @@ namespace osu.Game.Online.API.Requests
{
[JsonProperty("news_posts")]
public IEnumerable<APINewsPost> NewsPosts;
[JsonProperty("news_sidebar")]
public APINewsSidebar SidebarMetadata;
}
}

View File

@ -0,0 +1,20 @@
// 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 Newtonsoft.Json;
using System.Collections.Generic;
namespace osu.Game.Online.API.Requests.Responses
{
public class APINewsSidebar
{
[JsonProperty("current_year")]
public int CurrentYear { get; set; }
[JsonProperty("news_posts")]
public IEnumerable<APINewsPost> NewsPosts { get; set; }
[JsonProperty("years")]
public int[] Years { get; set; }
}
}

View File

@ -92,10 +92,6 @@ namespace osu.Game.Online.Multiplayer
[Resolved]
private UserLookupCache userLookupCache { get; set; } = null!;
// Only exists for compatibility with old osu-server-spectator build.
// Todo: Can be removed on 2021/02/26.
private long defaultPlaylistItemId;
private Room? apiRoom;
[BackgroundDependencyLoader]
@ -143,7 +139,6 @@ namespace osu.Game.Online.Multiplayer
{
Room = joinedRoom;
apiRoom = room;
defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0;
foreach (var user in joinedRoom.Users)
updateUserPlayingState(user.UserID, user.State);
}, cancellationSource.Token).ConfigureAwait(false);
@ -581,7 +576,7 @@ namespace osu.Game.Online.Multiplayer
void updateItem(PlaylistItem item)
{
item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId;
item.ID = settings.PlaylistItemId;
item.Beatmap.Value = beatmap;
item.Ruleset.Value = ruleset.RulesetInfo;
item.RequiredMods.Clear();

View File

@ -0,0 +1,179 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Graphics.Containers;
using osuTK;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using System.Diagnostics;
using osu.Framework.Platform;
namespace osu.Game.Overlays.News.Sidebar
{
public class MonthSection : CompositeDrawable
{
private const int animation_duration = 250;
public readonly BindableBool Expanded = new BindableBool();
public MonthSection(int month, int year, IEnumerable<APINewsPost> posts)
{
Debug.Assert(posts.All(p => p.PublishedAt.Month == month && p.PublishedAt.Year == year));
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new DropdownHeader(month, year)
{
Expanded = { BindTarget = Expanded }
},
new PostsContainer
{
Expanded = { BindTarget = Expanded },
Children = posts.Select(p => new PostButton(p)).ToArray()
}
}
};
}
private class DropdownHeader : OsuClickableContainer
{
public readonly BindableBool Expanded = new BindableBool();
private readonly SpriteIcon icon;
public DropdownHeader(int month, int year)
{
var date = new DateTime(year, month, 1);
RelativeSizeAxes = Axes.X;
Height = 15;
Action = Expanded.Toggle;
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = date.ToString("MMM yyyy")
},
icon = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Size = new Vector2(10),
Icon = FontAwesome.Solid.ChevronDown
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(open =>
{
icon.Scale = new Vector2(1, open.NewValue ? -1 : 1);
}, true);
}
}
private class PostButton : OsuHoverContainer
{
protected override IEnumerable<Drawable> EffectTargets => new[] { text };
private readonly TextFlowContainer text;
private readonly APINewsPost post;
public PostButton(APINewsPost post)
{
this.post = post;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Child = text = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12))
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Text = post.Title
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColours, GameHost host)
{
IdleColour = overlayColours.Light2;
HoverColour = overlayColours.Light1;
TooltipText = "view in browser";
Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug);
}
}
private class PostsContainer : Container
{
public readonly BindableBool Expanded = new BindableBool();
protected override Container<Drawable> Content { get; }
public PostsContainer()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
AutoSizeDuration = animation_duration;
AutoSizeEasing = Easing.Out;
InternalChild = Content = new FillFlowContainer
{
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Alpha = 0
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(updateState, true);
}
private void updateState(ValueChangedEvent<bool> expanded)
{
ClearTransforms(true);
if (expanded.NewValue)
{
AutoSizeAxes = Axes.Y;
Content.FadeIn(animation_duration, Easing.OutQuint);
}
else
{
AutoSizeAxes = Axes.None;
this.ResizeHeightTo(0, animation_duration, Easing.OutQuint);
Content.FadeOut(animation_duration, Easing.OutQuint);
}
}
}
}
}

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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Shapes;
using osuTK;
using System.Linq;
namespace osu.Game.Overlays.News.Sidebar
{
public class NewsSidebar : CompositeDrawable
{
[Cached]
public readonly Bindable<APINewsSidebar> Metadata = new Bindable<APINewsSidebar>();
private FillFlowContainer<MonthSection> monthsFlow;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.Y;
Width = 250;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Vertical = 20,
Left = 50,
Right = 30
},
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{
new YearsPanel(),
monthsFlow = new FillFlowContainer<MonthSection>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10)
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Metadata.BindValueChanged(onMetadataChanged, true);
}
private void onMetadataChanged(ValueChangedEvent<APINewsSidebar> metadata)
{
monthsFlow.Clear();
if (metadata.NewValue == null)
return;
var allPosts = metadata.NewValue.NewsPosts;
if (allPosts?.Any() != true)
return;
var lookup = metadata.NewValue.NewsPosts.ToLookup(post => post.PublishedAt.Month);
var keys = lookup.Select(kvp => kvp.Key);
var sortedKeys = keys.OrderByDescending(k => k).ToList();
var year = metadata.NewValue.CurrentYear;
for (int i = 0; i < sortedKeys.Count; i++)
{
var month = sortedKeys[i];
var posts = lookup[month];
monthsFlow.Add(new MonthSection(month, year, posts)
{
Expanded = { Value = i == 0 }
});
}
}
}
}

View File

@ -0,0 +1,113 @@
// 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.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.News.Sidebar
{
public class YearsPanel : CompositeDrawable
{
private readonly Bindable<APINewsSidebar> metadata = new Bindable<APINewsSidebar>();
private FillFlowContainer yearsFlow;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider overlayColours, Bindable<APINewsSidebar> metadata)
{
this.metadata.BindTo(metadata);
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
Masking = true;
CornerRadius = 6;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = overlayColours.Background3
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(5),
Child = yearsFlow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 5)
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
metadata.BindValueChanged(_ => recreateDrawables(), true);
}
private void recreateDrawables()
{
yearsFlow.Clear();
if (metadata.Value == null)
{
Hide();
return;
}
var currentYear = metadata.Value.CurrentYear;
foreach (var y in metadata.Value.Years)
yearsFlow.Add(new YearButton(y, y == currentYear));
Show();
}
public class YearButton : OsuHoverContainer
{
public int Year { get; }
private readonly bool isCurrent;
public YearButton(int year, bool isCurrent)
{
Year = year;
this.isCurrent = isCurrent;
RelativeSizeAxes = Axes.X;
Width = 0.25f;
Height = 15;
Child = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.GetFont(size: 12, weight: isCurrent ? FontWeight.SemiBold : FontWeight.Medium),
Text = year.ToString()
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
IdleColour = isCurrent ? Color4.White : colourProvider.Light2;
HoverColour = isCurrent ? Color4.White : colourProvider.Light1;
Action = () => { }; // Avoid button being disabled since there's no proper action assigned.
}
}
}
}

View File

@ -3,6 +3,7 @@
#nullable enable
using System;
using System.Diagnostics;
using osu.Framework.Graphics.Performance;
using osu.Framework.Graphics.Pooling;
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
/// <summary>
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
/// </summary>
protected TEntry? Entry { get; private set; }
public TEntry? Entry { get; private set; }
/// <summary>
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
@ -28,14 +29,28 @@ namespace osu.Game.Rulesets.Objects.Pooling
public override double LifetimeStart
{
get => base.LifetimeStart;
set => setLifetime(value, LifetimeEnd);
get => Entry?.LifetimeStart ?? double.MinValue;
set
{
if (Entry == null && LifetimeStart != value)
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
if (Entry != null)
Entry.LifetimeStart = value;
}
}
public override double LifetimeEnd
{
get => base.LifetimeEnd;
set => setLifetime(LifetimeStart, value);
get => Entry?.LifetimeEnd ?? double.MaxValue;
set
{
if (Entry == null && LifetimeEnd != value)
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
if (Entry != null)
Entry.LifetimeEnd = value;
}
}
public override bool RemoveWhenNotAlive => false;
@ -64,11 +79,8 @@ namespace osu.Game.Rulesets.Objects.Pooling
if (HasEntryApplied)
free();
setLifetime(entry.LifetimeStart, entry.LifetimeEnd);
Entry = entry;
OnApply(entry);
HasEntryApplied = true;
}
@ -95,27 +107,12 @@ namespace osu.Game.Rulesets.Objects.Pooling
{
}
private void setLifetime(double start, double end)
{
base.LifetimeStart = start;
base.LifetimeEnd = end;
if (Entry != null)
{
Entry.LifetimeStart = start;
Entry.LifetimeEnd = end;
}
}
private void free()
{
Debug.Assert(Entry != null && HasEntryApplied);
OnFree(Entry);
Entry = null;
setLifetime(double.MaxValue, double.MaxValue);
HasEntryApplied = false;
}
}

View File

@ -17,8 +17,18 @@ using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.UI
{
public class HitObjectContainer : LifetimeManagementContainer, IHitObjectContainer
public class HitObjectContainer : CompositeDrawable, IHitObjectContainer
{
/// <summary>
/// All entries in this <see cref="HitObjectContainer"/> including dead entries.
/// </summary>
public IEnumerable<HitObjectLifetimeEntry> Entries => allEntries;
/// <summary>
/// All alive entries and <see cref="DrawableHitObject"/>s used by the entries.
/// </summary>
public IEnumerable<(HitObjectLifetimeEntry Entry, DrawableHitObject Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
@ -60,8 +70,12 @@ namespace osu.Game.Rulesets.UI
internal double FutureLifetimeExtension { get; set; }
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> drawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> aliveDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
private readonly HashSet<HitObjectLifetimeEntry> allEntries = new HashSet<HitObjectLifetimeEntry>();
[Resolved(CanBeNull = true)]
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
@ -72,6 +86,7 @@ namespace osu.Game.Rulesets.UI
lifetimeManager.EntryBecameAlive += entryBecameAlive;
lifetimeManager.EntryBecameDead += entryBecameDead;
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
}
protected override void LoadAsyncComplete()
@ -84,93 +99,113 @@ namespace osu.Game.Rulesets.UI
#region Pooling support
public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry);
private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry);
private void addDrawable(HitObjectLifetimeEntry entry)
public void Add(HitObjectLifetimeEntry entry)
{
Debug.Assert(!drawableMap.ContainsKey(entry));
allEntries.Add(entry);
lifetimeManager.AddEntry(entry);
}
var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
public bool Remove(HitObjectLifetimeEntry entry)
{
if (!lifetimeManager.RemoveEntry(entry)) return false;
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry.
if (nonPooledDrawableMap.Remove(entry, out var drawable))
removeDrawable(drawable);
allEntries.Remove(entry);
return true;
}
private void entryBecameAlive(LifetimeEntry lifetimeEntry)
{
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
Debug.Assert(!aliveDrawableMap.ContainsKey(entry));
bool isNonPooled = nonPooledDrawableMap.TryGetValue(entry, out var drawable);
drawable ??= pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
if (drawable == null)
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
aliveDrawableMap[entry] = drawable;
OnAdd(drawable);
if (isNonPooled) return;
addDrawable(drawable);
HitObjectUsageBegan?.Invoke(entry.HitObject);
}
private void entryBecameDead(LifetimeEntry lifetimeEntry)
{
var entry = (HitObjectLifetimeEntry)lifetimeEntry;
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
var drawable = aliveDrawableMap[entry];
bool isNonPooled = nonPooledDrawableMap.ContainsKey(entry);
drawable.OnKilled();
aliveDrawableMap.Remove(entry);
OnRemove(drawable);
if (isNonPooled) return;
removeDrawable(drawable);
// The hit object is not freed when the DHO was not pooled.
HitObjectUsageFinished?.Invoke(entry.HitObject);
}
private void addDrawable(DrawableHitObject drawable)
{
drawable.OnNewResult += onNewResult;
drawable.OnRevertResult += onRevertResult;
bindStartTime(drawable);
AddInternal(drawableMap[entry] = drawable, false);
OnAdd(drawable);
HitObjectUsageBegan?.Invoke(entry.HitObject);
AddInternal(drawable);
}
private void removeDrawable(HitObjectLifetimeEntry entry)
private void removeDrawable(DrawableHitObject drawable)
{
Debug.Assert(drawableMap.ContainsKey(entry));
var drawable = drawableMap[entry];
// OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
drawable.OnKilled();
drawable.OnNewResult -= onNewResult;
drawable.OnRevertResult -= onRevertResult;
drawableMap.Remove(entry);
OnRemove(drawable);
unbindStartTime(drawable);
RemoveInternal(drawable);
HitObjectUsageFinished?.Invoke(entry.HitObject);
RemoveInternal(drawable);
}
#endregion
#region Non-pooling support
public virtual void Add(DrawableHitObject hitObject)
public virtual void Add(DrawableHitObject drawable)
{
bindStartTime(hitObject);
if (drawable.Entry == null)
throw new InvalidOperationException($"May not add a {nameof(DrawableHitObject)} without {nameof(HitObject)} associated");
hitObject.OnNewResult += onNewResult;
hitObject.OnRevertResult += onRevertResult;
AddInternal(hitObject);
OnAdd(hitObject);
nonPooledDrawableMap.Add(drawable.Entry, drawable);
addDrawable(drawable);
Add(drawable.Entry);
}
public virtual bool Remove(DrawableHitObject hitObject)
public virtual bool Remove(DrawableHitObject drawable)
{
OnRemove(hitObject);
if (!RemoveInternal(hitObject))
if (drawable.Entry == null)
return false;
hitObject.OnNewResult -= onNewResult;
hitObject.OnRevertResult -= onRevertResult;
unbindStartTime(hitObject);
return true;
return Remove(drawable.Entry);
}
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
private void entryCrossedBoundary(LifetimeEntry entry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
{
if (!(e.Child is DrawableHitObject hitObject))
return;
if (nonPooledDrawableMap.TryGetValue((HitObjectLifetimeEntry)entry, out var drawable))
OnChildLifetimeBoundaryCrossed(new LifetimeBoundaryCrossedEvent(drawable, kind, direction));
}
if ((e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward)
|| (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward))
{
hitObject.OnKilled();
}
protected virtual void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
{
}
#endregion
@ -195,12 +230,13 @@ namespace osu.Game.Rulesets.UI
{
}
public virtual void Clear(bool disposeChildren = true)
public virtual void Clear()
{
lifetimeManager.ClearEntries();
ClearInternal(disposeChildren);
unbindAllStartTimes();
foreach (var drawable in nonPooledDrawableMap.Values)
removeDrawable(drawable);
nonPooledDrawableMap.Clear();
Debug.Assert(InternalChildren.Count == 0 && startTimeMap.Count == 0 && aliveDrawableMap.Count == 0, "All hit objects should have been removed");
}
protected override bool CheckChildrenLife()

View File

@ -354,8 +354,11 @@ namespace osu.Game.Rulesets.UI
// If this is the first time this DHO is being used, then apply the DHO mods.
// This is done before Apply() so that the state is updated once when the hitobject is applied.
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
if (mods != null)
{
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
m.ApplyToDrawableHitObjects(dho.Yield());
}
}
if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry))

View File

@ -50,9 +50,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
timeRange.ValueChanged += _ => layoutCache.Invalidate();
}
public override void Clear(bool disposeChildren = true)
public override void Clear()
{
base.Clear(disposeChildren);
base.Clear();
toComputeLifetime.Clear();
layoutComputed.Clear();

View File

@ -299,6 +299,13 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
}
/// <summary>
/// Retrieves an item's blueprint.
/// </summary>
/// <param name="item">The item to retrieve the blueprint of.</param>
/// <returns>The blueprint.</returns>
protected SelectionBlueprint<T> GetBlueprintFor(T item) => blueprintMap[item];
#endregion
#region Selection

View File

@ -16,6 +16,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osuTK;
@ -73,6 +74,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
}
protected override void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
{
base.TransferBlueprintFor(hitObject, drawableObject);
var blueprint = (HitObjectSelectionBlueprint)GetBlueprintFor(hitObject);
blueprint.DrawableObject = drawableObject;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.ControlPressed)

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Screens.Edit.Compose.Components
{
@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected readonly HitObjectComposer Composer;
private HitObjectUsageEventBuffer usageEventBuffer;
protected EditorBlueprintContainer(HitObjectComposer composer)
{
Composer = composer;
@ -45,11 +48,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
Composer.Playfield.HitObjectUsageBegan += AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished += RemoveBlueprintFor;
usageEventBuffer = new HitObjectUsageEventBuffer(Composer.Playfield);
usageEventBuffer.HitObjectUsageBegan += AddBlueprintFor;
usageEventBuffer.HitObjectUsageFinished += RemoveBlueprintFor;
usageEventBuffer.HitObjectUsageTransferred += TransferBlueprintFor;
}
}
protected override void Update()
{
base.Update();
usageEventBuffer?.Update();
}
protected override IEnumerable<SelectionBlueprint<HitObject>> SortForMovement(IReadOnlyList<SelectionBlueprint<HitObject>> blueprints)
=> blueprints.OrderBy(b => b.Item.StartTime);
@ -80,6 +91,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
base.AddBlueprintFor(item);
}
/// <summary>
/// Invoked when a <see cref="HitObject"/> has been transferred to another <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The hit object which has been assigned to a new drawable.</param>
/// <param name="drawableObject">The new drawable that is representing the hit object.</param>
protected virtual void TransferBlueprintFor(HitObject hitObject, DrawableHitObject drawableObject)
{
}
protected override void DragOperationCompleted()
{
base.DragOperationCompleted();
@ -133,11 +153,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
}
if (Composer != null)
{
Composer.Playfield.HitObjectUsageBegan -= AddBlueprintFor;
Composer.Playfield.HitObjectUsageFinished -= RemoveBlueprintFor;
}
usageEventBuffer?.Dispose();
}
}
}

View File

@ -350,5 +350,55 @@ namespace osu.Game.Screens.Edit.Compose.Components
=> Enumerable.Empty<MenuItem>();
#endregion
#region Helper Methods
/// <summary>
/// Given a flip direction, a surrounding quad for all selected objects, and a position,
/// will return the flipped position in screen space coordinates.
/// </summary>
protected static Vector2 GetFlippedPosition(Direction direction, Quad quad, Vector2 position)
{
var centre = quad.Centre;
switch (direction)
{
case Direction.Horizontal:
position.X = centre.X - (position.X - centre.X);
break;
case Direction.Vertical:
position.Y = centre.Y - (position.Y - centre.Y);
break;
}
return position;
}
/// <summary>
/// Returns a quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
protected static Quad GetSurroundingQuad(IEnumerable<Vector2> points)
{
if (!points.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
#endregion
}
}

View File

@ -0,0 +1,83 @@
// 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 JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Screens.Edit.Compose
{
/// <summary>
/// Buffers events from the many <see cref="HitObjectContainer"/>s in a nested <see cref="Playfield"/> hierarchy
/// to ensure correct ordering of events.
/// </summary>
internal class HitObjectUsageEventBuffer : IDisposable
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes used by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If the ruleset uses pooled objects, this represents the time when the <see cref="HitObject"/>s become alive.
/// </remarks>
public event Action<HitObject> HitObjectUsageBegan;
/// <summary>
/// Invoked when a <see cref="HitObject"/> becomes unused by a <see cref="DrawableHitObject"/>.
/// </summary>
/// <remarks>
/// If the ruleset uses pooled objects, this represents the time when the <see cref="HitObject"/>s become dead.
/// </remarks>
public event Action<HitObject> HitObjectUsageFinished;
/// <summary>
/// Invoked when a <see cref="HitObject"/> has been transferred to another <see cref="DrawableHitObject"/>.
/// </summary>
public event Action<HitObject, DrawableHitObject> HitObjectUsageTransferred;
private readonly Playfield playfield;
/// <summary>
/// Creates a new <see cref="HitObjectUsageEventBuffer"/>.
/// </summary>
/// <param name="playfield">The most top-level <see cref="Playfield"/>.</param>
public HitObjectUsageEventBuffer([NotNull] Playfield playfield)
{
this.playfield = playfield;
playfield.HitObjectUsageBegan += onHitObjectUsageBegan;
playfield.HitObjectUsageFinished += onHitObjectUsageFinished;
}
private readonly List<HitObject> usageFinishedHitObjects = new List<HitObject>();
private void onHitObjectUsageBegan(HitObject hitObject)
{
if (usageFinishedHitObjects.Remove(hitObject))
HitObjectUsageTransferred?.Invoke(hitObject, playfield.AllHitObjects.Single(d => d.HitObject == hitObject));
else
HitObjectUsageBegan?.Invoke(hitObject);
}
private void onHitObjectUsageFinished(HitObject hitObject) => usageFinishedHitObjects.Add(hitObject);
public void Update()
{
foreach (var hitObject in usageFinishedHitObjects)
HitObjectUsageFinished?.Invoke(hitObject);
usageFinishedHitObjects.Clear();
}
public void Dispose()
{
if (playfield != null)
{
playfield.HitObjectUsageBegan -= onHitObjectUsageBegan;
playfield.HitObjectUsageFinished -= onHitObjectUsageFinished;
}
}
}
}

View File

@ -929,11 +929,11 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param>
/// <returns>The imported score.</returns>
protected virtual Task ImportScore(Score score)
protected virtual async Task ImportScore(Score score)
{
// Replays are already populated and present in the game's database, so should not be re-imported.
if (DrawableRuleset.ReplayScore != null)
return Task.CompletedTask;
return;
LegacyByteArrayReader replayReader;
@ -943,7 +943,18 @@ namespace osu.Game.Screens.Play
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
return scoreManager.Import(score.ScoreInfo, replayReader);
// For the time being, online ID responses are not really useful for anything.
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
long? onlineScoreId = score.ScoreInfo.OnlineScoreID;
score.ScoreInfo.OnlineScoreID = null;
await scoreManager.Import(score.ScoreInfo, replayReader).ConfigureAwait(false);
// ... And restore the online ID for other processes to handle correctly (e.g. de-duplication for the results screen).
score.ScoreInfo.OnlineScoreID = onlineScoreId;
}
/// <summary>

View File

@ -116,12 +116,7 @@ namespace osu.Game.Screens.Play
request.Success += s =>
{
// For the time being, online ID responses are not really useful for anything.
// In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores.
//
// Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint
// conflicts across various systems (ie. solo and multiplayer).
// score.ScoreInfo.OnlineScoreID = s.ID;
score.ScoreInfo.OnlineScoreID = s.ID;
tcs.SetResult(true);
};

View File

@ -124,7 +124,7 @@ namespace osu.Game.Skinning.Editor
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => drawable.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 ScreenSpaceSelectionPoint => drawable.ScreenSpaceDrawQuad.Centre;
public override Vector2 ScreenSpaceSelectionPoint => drawable.ToScreenSpace(drawable.OriginPosition);
public override Quad SelectionQuad => drawable.ScreenSpaceDrawQuad;
}

View File

@ -43,10 +43,16 @@ namespace osu.Game.Skinning.Editor
public override bool HandleFlip(Direction direction)
{
// TODO: this is temporary as well.
foreach (var c in SelectedBlueprints)
var selectionQuad = GetSurroundingQuad(SelectedBlueprints.Select(b => b.ScreenSpaceSelectionPoint));
foreach (var b in SelectedBlueprints)
{
((Drawable)c.Item).Scale *= new Vector2(
var drawableItem = (Drawable)b.Item;
drawableItem.Position =
drawableItem.Parent.ToLocalSpace(GetFlippedPosition(direction, selectionQuad, b.ScreenSpaceSelectionPoint)) - drawableItem.AnchorPosition;
drawableItem.Scale *= new Vector2(
direction == Direction.Horizontal ? -1 : 1,
direction == Direction.Vertical ? -1 : 1
);