Merge branch 'master' into beatmap-serialization

This commit is contained in:
Dan Balasescu 2017-12-22 07:38:05 +09:00 committed by GitHub
commit a3d947301b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 526 additions and 47 deletions

View File

@ -93,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
float approxFollowCircleRadius = (float)(slider.Radius * 3);
var computeVertex = new Action<double>(t =>
{
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
var diff = slider.PositionAt(t) - slider.LazyEndPosition.Value;
float dist = diff.Length;

View File

@ -6,14 +6,44 @@ using osu.Framework.Graphics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
using osu.Framework.Allocation;
using OpenTK;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual
{
[Description("PlaySongSelect leaderboard")]
public class TestCaseLeaderboard : OsuTestCase
{
private readonly Leaderboard leaderboard;
private RulesetStore rulesets;
private readonly FailableLeaderboard leaderboard;
public TestCaseLeaderboard()
{
Add(leaderboard = new FailableLeaderboard
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
Scope = LeaderboardScope.Global,
});
AddStep(@"New Scores", newScores);
AddStep(@"Empty Scores", () => leaderboard.SetRetrievalState(PlaceholderState.NoScores));
AddStep(@"Network failure", () => leaderboard.SetRetrievalState(PlaceholderState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetRetrievalState(PlaceholderState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetRetrievalState(PlaceholderState.NotLoggedIn));
AddStep(@"Real beatmap", realBeatmap);
}
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
this.rulesets = rulesets;
}
private void newScores()
{
@ -204,17 +234,44 @@ namespace osu.Game.Tests.Visual
leaderboard.Scores = scores;
}
public TestCaseLeaderboard()
private void realBeatmap()
{
Add(leaderboard = new Leaderboard
leaderboard.Beatmap = new BeatmapInfo
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Size = new Vector2(550f, 450f),
});
StarDifficulty = 1.36,
Version = @"BASIC",
OnlineBeatmapID = 1113057,
Ruleset = rulesets.GetRuleset(0),
BaseDifficulty = new BeatmapDifficulty
{
CircleSize = 4,
DrainRate = 6.5f,
OverallDifficulty = 6.5f,
ApproachRate = 5,
},
OnlineInfo = new BeatmapOnlineInfo
{
Length = 115000,
CircleCount = 265,
SliderCount = 71,
PlayCount = 47906,
PassCount = 19899,
},
Metrics = new BeatmapMetrics
{
Ratings = Enumerable.Range(0, 11),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
},
};
}
AddStep(@"New Scores", newScores);
newScores();
private class FailableLeaderboard : Leaderboard
{
public void SetRetrievalState(PlaceholderState state)
{
PlaceholderState = state;
}
}
}
}

View File

@ -1,6 +1,8 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
@ -8,16 +10,30 @@ namespace osu.Game.Tests.Visual
public class TestCaseSettings : OsuTestCase
{
private readonly SettingsOverlay settings;
private readonly DialogOverlay dialogOverlay;
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent);
public TestCaseSettings()
{
Children = new[] { settings = new MainSettings() };
settings = new MainSettings
{
State = Visibility.Visible
};
Add(dialogOverlay = new DialogOverlay
{
Depth = -1
});
}
protected override void LoadComplete()
[BackgroundDependencyLoader]
private void load()
{
base.LoadComplete();
settings.ToggleVisibility();
dependencies.Cache(dialogOverlay);
Add(settings);
}
}
}

View File

@ -341,6 +341,61 @@ namespace osu.Game.Beatmaps
}
}
public void UndeleteAll()
{
var deleteMaps = QueryBeatmapSets(bs => bs.DeletePending).ToList();
if (!deleteMaps.Any()) return;
var notification = new ProgressNotification
{
CompletionText = "Restored all deleted beatmaps!",
Progress = 0,
State = ProgressNotificationState.Active,
};
PostNotification?.Invoke(notification);
int i = 0;
foreach (var bs in deleteMaps)
{
if (notification.State == ProgressNotificationState.Cancelled)
// user requested abort
return;
notification.Text = $"Restoring ({i} of {deleteMaps.Count})";
notification.Progress = (float)++i / deleteMaps.Count;
Undelete(bs);
}
notification.State = ProgressNotificationState.Completed;
}
public void Undelete(BeatmapSetInfo beatmapSet)
{
if (beatmapSet.Protected)
return;
lock (importContext)
{
var context = importContext.Value;
using (var transaction = context.BeginTransaction())
{
context.ChangeTracker.AutoDetectChangesEnabled = false;
var iFiles = new FileStore(() => context, storage);
var iBeatmaps = createBeatmapStore(() => context);
undelete(iBeatmaps, iFiles, beatmapSet);
context.ChangeTracker.AutoDetectChangesEnabled = true;
context.SaveChanges(transaction);
}
}
}
/// <summary>
/// Delete a beatmap difficulty.
/// </summary>

View File

@ -10,19 +10,28 @@ using osu.Game.Rulesets;
using osu.Game.Users;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Framework.IO.Network;
namespace osu.Game.Online.API.Requests
{
public class GetScoresRequest : APIRequest<GetScoresResponse>
{
private readonly BeatmapInfo beatmap;
private readonly LeaderboardScope scope;
private readonly RulesetInfo ruleset;
public GetScoresRequest(BeatmapInfo beatmap)
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, LeaderboardScope scope = LeaderboardScope.Global)
{
if (!beatmap.OnlineBeatmapID.HasValue)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}.");
if (scope == LeaderboardScope.Local)
throw new InvalidOperationException("Should not attempt to request online scores for a local scoped leaderboard");
this.beatmap = beatmap;
this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
Success += onSuccess;
}
@ -33,6 +42,17 @@ namespace osu.Game.Online.API.Requests
score.ApplyBeatmap(beatmap);
}
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Timeout = 30000;
req.AddParameter(@"type", scope.ToString().ToLowerInvariant());
req.AddParameter(@"mode", ruleset.ShortName);
return req;
}
protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}/scores";
}

View File

@ -104,7 +104,7 @@ namespace osu.Game.Overlays
scores.IsLoading = true;
getScoresRequest = new GetScoresRequest(beatmap);
getScoresRequest = new GetScoresRequest(beatmap, beatmap.Ruleset);
getScoresRequest.Success += r =>
{
scores.Scores = r.Scores;

View File

@ -31,6 +31,8 @@ namespace osu.Game.Overlays.Mods
protected readonly OsuSpriteText MultiplierLabel;
private readonly FillFlowContainer footerContainer;
protected override bool BlockPassThroughKeyboard => false;
protected readonly FillFlowContainer<ModSection> ModSectionsContainer;
public readonly Bindable<IEnumerable<Mod>> SelectedMods = new Bindable<IEnumerable<Mod>>();

View File

@ -13,7 +13,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
using System.Threading;
namespace osu.Game.Overlays.Music
{
@ -153,11 +152,6 @@ namespace osu.Game.Overlays.Music
var track = beatmapBacking.Value.Track;
track.Restart();
// this is temporary until we have blocking (async.Wait()) audio component methods.
// then we can call RestartAsync().Wait() or the blocking version above.
while (!track.IsRunning)
Thread.Sleep(1);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Settings
{
/// <summary>
/// A <see cref="SettingsButton"/> with pink colours to mark dangerous/destructive actions.
/// </summary>
public class DangerousSettingsButton : SettingsButton
{
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.Pink;
Triangles.ColourDark = colours.PinkDark;
Triangles.ColourLight = colours.PinkLight;
}
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Game.Graphics;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
public class DeleteAllBeatmapsDialog : PopupDialog
{
public DeleteAllBeatmapsDialog(Action deleteAction)
{
BodyText = "Everything?";
Icon = FontAwesome.fa_trash_o;
HeaderText = @"Confirm deletion of";
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Yes. Go for it.",
Action = deleteAction
},
new PopupDialogCancelButton
{
Text = @"No! Abort mission!",
},
};
}
}
}

View File

@ -15,12 +15,12 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private TriangleButton importButton;
private TriangleButton deleteButton;
private TriangleButton restoreButton;
private TriangleButton migrateButton;
private TriangleButton undeleteButton;
protected override string Header => "General";
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
private void load(BeatmapManager beatmaps, DialogOverlay dialogOverlay)
{
Children = new Drawable[]
{
@ -34,13 +34,16 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
.ContinueWith(t => Schedule(() => importButton.Enabled.Value = true), TaskContinuationOptions.LongRunning);
}
},
deleteButton = new SettingsButton
deleteButton = new DangerousSettingsButton
{
Text = "Delete ALL beatmaps",
Action = () =>
{
deleteButton.Enabled.Value = false;
Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteButton.Enabled.Value = false;
Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true));
}));
}
},
restoreButton = new SettingsButton
@ -55,7 +58,16 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
beatmaps.Restore(b);
}).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true));
}
}
},
undeleteButton = new SettingsButton
{
Text = "Restore all recently deleted beatmaps",
Action = () =>
{
undeleteButton.Enabled.Value = false;
Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true));
}
},
};
}
}

View File

@ -28,6 +28,8 @@ namespace osu.Game.Overlays
private readonly Container contentContainer;
protected override bool BlockPassThroughKeyboard => true;
protected override Container<Drawable> Content => contentContainer;
protected Color4 FirstWaveColour

View File

@ -17,7 +17,6 @@ using osu.Game.Rulesets.UI;
using osu.Game.Screens.Backgrounds;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Threading;
using osu.Game.Rulesets.Mods;
@ -327,11 +326,6 @@ namespace osu.Game.Screens.Play
{
adjustableSourceClock.Reset();
// this is temporary until we have blocking (async.Wait()) audio component methods.
// then we can call ResetAsync().Wait() or the blocking version above.
while (adjustableSourceClock.IsRunning)
Thread.Sleep(1);
Schedule(() =>
{
decoupledClock.ChangeSource(adjustableSourceClock);

View File

@ -52,6 +52,7 @@ namespace osu.Game.Screens.Select
default:
Details.Hide();
Leaderboard.Scope = (LeaderboardScope)tab - 1;
Leaderboard.Show();
break;
}

View File

@ -18,14 +18,26 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using System.Linq;
using osu.Framework.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Logging;
using osu.Game.Rulesets;
using osu.Framework.Input;
namespace osu.Game.Screens.Select.Leaderboards
{
public class Leaderboard : Container
{
private const double fade_duration = 200;
private readonly ScrollContainer scrollContainer;
private readonly Container placeholderContainer;
private FillFlowContainer<LeaderboardScore> scrollFlow;
private readonly Bindable<RulesetInfo> ruleset = new Bindable<RulesetInfo>();
public Action<Score> ScoreSelected;
private readonly LoadingAnimation loading;
@ -38,15 +50,18 @@ namespace osu.Game.Screens.Select.Leaderboards
set
{
scores = value;
getScoresRequest?.Cancel();
scrollFlow?.FadeOut(200);
scrollFlow?.Expire();
scrollFlow?.FadeOut(fade_duration).Expire();
scrollFlow = null;
if (scores == null)
loading.Hide();
if (scores == null || !scores.Any())
return;
// ensure placeholder is hidden when displaying scores
PlaceholderState = PlaceholderState.Successful;
// schedule because we may not be loaded yet (LoadComponentAsync complains).
Schedule(() =>
{
@ -74,6 +89,55 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
private LeaderboardScope scope;
public LeaderboardScope Scope
{
get { return scope; }
set
{
if (value == scope) return;
scope = value;
updateScores();
}
}
private PlaceholderState placeholderState;
protected PlaceholderState PlaceholderState
{
get { return placeholderState; }
set
{
if (value == placeholderState) return;
switch (placeholderState = value)
{
case PlaceholderState.NetworkFailure:
replacePlaceholder(new RetrievalFailurePlaceholder
{
OnRetry = updateScores,
});
break;
case PlaceholderState.NoScores:
replacePlaceholder(new MessagePlaceholder(@"No records yet!"));
break;
case PlaceholderState.NotLoggedIn:
replacePlaceholder(new MessagePlaceholder(@"Please login to view online leaderboards!"));
break;
case PlaceholderState.NotSupporter:
replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!"));
break;
default:
replacePlaceholder(null);
break;
}
}
}
public Leaderboard()
{
Children = new Drawable[]
@ -83,7 +147,14 @@ namespace osu.Game.Screens.Select.Leaderboards
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
},
loading = new LoadingAnimation()
loading = new LoadingAnimation(),
placeholderContainer = new Container
{
Alpha = 0,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
@ -91,6 +162,8 @@ namespace osu.Game.Screens.Select.Leaderboards
private BeatmapInfo beatmap;
private OsuGame osuGame;
private ScheduledDelegate pendingBeatmapSwitch;
public BeatmapInfo Beatmap
@ -109,33 +182,115 @@ namespace osu.Game.Screens.Select.Leaderboards
}
[BackgroundDependencyLoader(permitNulls: true)]
private void load(APIAccess api)
private void load(APIAccess api, OsuGame osuGame)
{
this.api = api;
this.osuGame = osuGame;
if (osuGame != null)
ruleset.BindTo(osuGame.Ruleset);
ruleset.ValueChanged += r => updateScores();
if (api != null)
api.OnStateChange += handleApiStateChange;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (api != null)
api.OnStateChange -= handleApiStateChange;
}
private GetScoresRequest getScoresRequest;
private void handleApiStateChange(APIState oldState, APIState newState)
{
if (Scope == LeaderboardScope.Local)
// No need to respond to API state change while current scope is local
return;
if (newState == APIState.Online)
updateScores();
}
private void updateScores()
{
if (!IsLoaded) return;
Scores = null;
getScoresRequest?.Cancel();
getScoresRequest = null;
Scores = null;
if (api == null || Beatmap?.OnlineBeatmapID == null) return;
if (Scope == LeaderboardScope.Local)
{
// TODO: get local scores from wherever here.
PlaceholderState = PlaceholderState.NoScores;
return;
}
if (api?.IsLoggedIn != true)
{
PlaceholderState = PlaceholderState.NotLoggedIn;
return;
}
if (Beatmap?.OnlineBeatmapID == null)
{
PlaceholderState = PlaceholderState.NetworkFailure;
return;
}
PlaceholderState = PlaceholderState.Retrieving;
loading.Show();
getScoresRequest = new GetScoresRequest(Beatmap);
if (Scope != LeaderboardScope.Global && !api.LocalUser.Value.IsSupporter)
{
loading.Hide();
PlaceholderState = PlaceholderState.NotSupporter;
return;
}
getScoresRequest = new GetScoresRequest(Beatmap, osuGame?.Ruleset.Value ?? Beatmap.Ruleset, Scope);
getScoresRequest.Success += r =>
{
Scores = r.Scores;
loading.Hide();
PlaceholderState = Scores.Any() ? PlaceholderState.Successful : PlaceholderState.NoScores;
};
getScoresRequest.Failure += onUpdateFailed;
api.Queue(getScoresRequest);
}
private void onUpdateFailed(Exception e)
{
if (e is OperationCanceledException) return;
PlaceholderState = PlaceholderState.NetworkFailure;
Logger.Error(e, @"Couldn't fetch beatmap scores!");
}
private void replacePlaceholder(Placeholder placeholder)
{
if (placeholder == null)
{
placeholderContainer.FadeOutFromOne(fade_duration, Easing.OutQuint);
placeholderContainer.Clear(true);
return;
}
var existingPlaceholder = placeholderContainer.Children.FirstOrDefault() as Placeholder;
if (placeholder.Equals(existingPlaceholder))
return;
Scores = null;
placeholderContainer.Clear(true);
placeholderContainer.Child = placeholder;
placeholderContainer.FadeInFromZero(fade_duration, Easing.OutQuint);
}
protected override void Update()
{
base.Update();
@ -164,5 +319,118 @@ namespace osu.Game.Screens.Select.Leaderboards
}
}
}
private abstract class Placeholder : FillFlowContainer, IEquatable<Placeholder>
{
public virtual bool Equals(Placeholder other) => GetType() == other?.GetType();
}
private class MessagePlaceholder : Placeholder
{
private readonly string message;
public MessagePlaceholder(string message)
{
Direction = FillDirection.Horizontal;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new SpriteIcon
{
Icon = FontAwesome.fa_exclamation_circle,
Size = new Vector2(26),
Margin = new MarginPadding { Right = 10 },
},
new OsuSpriteText
{
Text = this.message = message,
TextSize = 22,
},
};
}
public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message;
}
private class RetrievalFailurePlaceholder : Placeholder
{
public Action OnRetry;
public RetrievalFailurePlaceholder()
{
Direction = FillDirection.Horizontal;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
new RetryButton
{
Action = () => OnRetry?.Invoke(),
Margin = new MarginPadding { Right = 10 },
},
new OsuSpriteText
{
Anchor = Anchor.TopLeft,
Text = @"Couldn't retrieve scores!",
TextSize = 22,
},
};
}
private class RetryButton : OsuHoverContainer
{
private readonly SpriteIcon icon;
public Action Action;
public RetryButton()
{
Height = 26;
Width = 26;
Child = new OsuClickableContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Action = () => Action?.Invoke(),
Child = icon = new SpriteIcon
{
Icon = FontAwesome.fa_refresh,
Size = new Vector2(26),
Shadow = true,
},
};
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
icon.ScaleTo(0.8f, 4000, Easing.OutQuint);
return base.OnMouseDown(state, args);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{
icon.ScaleTo(1, 1000, Easing.OutElastic);
return base.OnMouseUp(state, args);
}
}
}
}
public enum LeaderboardScope
{
Local,
Country,
Global,
Friend,
}
public enum PlaceholderState
{
Successful,
Retrieving,
NetworkFailure,
NoScores,
NotLoggedIn,
NotSupporter,
}
}

View File

@ -275,7 +275,7 @@ namespace osu.Game.Screens.Select
if (beatmap == Beatmap.Value.BeatmapInfo)
performLoad();
else
selectionChangedDebounce = Scheduler.AddDelayed(performLoad, 100);
selectionChangedDebounce = Scheduler.AddDelayed(performLoad, 200);
}
}

View File

@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual
if (!api.IsLoggedIn)
return;
lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo);
lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo, newBeatmap.BeatmapInfo.Ruleset);
lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new PerformanceDisplay(s, newBeatmap.Beatmap)));
api.Queue(lastRequest);
}

View File

@ -268,6 +268,7 @@
<Compile Include="Beatmaps\Formats\LegacyStoryboardDecoder.cs" />
<Compile Include="Database\DatabaseContextFactory.cs" />
<Compile Include="Database\IHasPrimaryKey.cs" />
<Compile Include="Overlays\Settings\DangerousSettingsButton.cs" />
<Compile Include="Graphics\UserInterface\HoverClickSounds.cs" />
<Compile Include="Graphics\UserInterface\HoverSounds.cs" />
<Compile Include="Graphics\UserInterface\OsuButton.cs" />
@ -308,6 +309,7 @@
<Compile Include="Overlays\Profile\Sections\Ranks\PaginatedScoreContainer.cs" />
<Compile Include="Overlays\Profile\Sections\Ranks\DrawableTotalScore.cs" />
<Compile Include="Overlays\Profile\Sections\Ranks\ScoreModsContainer.cs" />
<Compile Include="Overlays\Settings\Sections\Maintenance\DeleteAllBeatmapsDialog.cs" />
<Compile Include="Overlays\Settings\SettingsButton.cs" />
<Compile Include="Rulesets\Edit\Layers\Selection\OriginHandle.cs" />
<Compile Include="Rulesets\Edit\Layers\Selection\HitObjectSelectionBox.cs" />