mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 14:12:56 +08:00
Merge branch 'fix-broken-test-scene' into spectator-state-rework
This commit is contained in:
commit
09728a29ed
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.128.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.204.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -27,7 +27,7 @@ namespace osu.Android
|
|||||||
{
|
{
|
||||||
gameActivity.RunOnUiThread(() =>
|
gameActivity.RunOnUiThread(() =>
|
||||||
{
|
{
|
||||||
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
|
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -8,16 +9,18 @@ using System.Threading.Tasks;
|
|||||||
using Android.App;
|
using Android.App;
|
||||||
using Android.Content;
|
using Android.Content;
|
||||||
using Android.Content.PM;
|
using Android.Content.PM;
|
||||||
using Android.Net;
|
using Android.Graphics;
|
||||||
using Android.OS;
|
using Android.OS;
|
||||||
using Android.Provider;
|
using Android.Provider;
|
||||||
using Android.Views;
|
using Android.Views;
|
||||||
using osu.Framework.Android;
|
using osu.Framework.Android;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
|
using Debug = System.Diagnostics.Debug;
|
||||||
|
using Uri = Android.Net.Uri;
|
||||||
|
|
||||||
namespace osu.Android
|
namespace osu.Android
|
||||||
{
|
{
|
||||||
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser)]
|
[Activity(ConfigurationChanges = DEFAULT_CONFIG_CHANGES, Exported = true, LaunchMode = DEFAULT_LAUNCH_MODE, MainLauncher = true)]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")]
|
||||||
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
[IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osr", DataHost = "*", DataMimeType = "*/*")]
|
||||||
@ -41,6 +44,12 @@ namespace osu.Android
|
|||||||
{
|
{
|
||||||
private static readonly string[] osu_url_schemes = { "osu", "osump" };
|
private static readonly string[] osu_url_schemes = { "osu", "osump" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default screen orientation.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Adjusted on startup to match expected UX for the current device type (phone/tablet).</remarks>
|
||||||
|
public ScreenOrientation DefaultOrientation = ScreenOrientation.Unspecified;
|
||||||
|
|
||||||
private OsuGameAndroid game;
|
private OsuGameAndroid game;
|
||||||
|
|
||||||
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this);
|
||||||
@ -54,8 +63,20 @@ namespace osu.Android
|
|||||||
// reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
|
// reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent)
|
||||||
handleIntent(Intent);
|
handleIntent(Intent);
|
||||||
|
|
||||||
|
Debug.Assert(Window != null);
|
||||||
|
|
||||||
Window.AddFlags(WindowManagerFlags.Fullscreen);
|
Window.AddFlags(WindowManagerFlags.Fullscreen);
|
||||||
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
|
Window.AddFlags(WindowManagerFlags.KeepScreenOn);
|
||||||
|
|
||||||
|
Debug.Assert(WindowManager?.DefaultDisplay != null);
|
||||||
|
Debug.Assert(Resources?.DisplayMetrics != null);
|
||||||
|
|
||||||
|
Point displaySize = new Point();
|
||||||
|
WindowManager.DefaultDisplay.GetSize(displaySize);
|
||||||
|
float smallestWidthDp = Math.Min(displaySize.X, displaySize.Y) / Resources.DisplayMetrics.Density;
|
||||||
|
bool isTablet = smallestWidthDp >= 600f;
|
||||||
|
|
||||||
|
RequestedOrientation = DefaultOrientation = isTablet ? ScreenOrientation.FullUser : ScreenOrientation.SensorLandscape;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnNewIntent(Intent intent) => handleIntent(intent);
|
protected override void OnNewIntent(Intent intent) => handleIntent(intent);
|
||||||
@ -104,7 +125,7 @@ namespace osu.Android
|
|||||||
|
|
||||||
cursor.MoveToFirst();
|
cursor.MoveToFirst();
|
||||||
|
|
||||||
var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
|
int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
|
||||||
string filename = cursor.GetString(filenameColumn);
|
string filename = cursor.GetString(filenameColumn);
|
||||||
|
|
||||||
// SharpCompress requires archive streams to be seekable, which the stream opened by
|
// SharpCompress requires archive streams to be seekable, which the stream opened by
|
||||||
|
@ -24,11 +24,16 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
@ -24,11 +24,16 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
@ -370,21 +370,21 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
|
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}, true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new UnstableRate(score.HitEvents)
|
new UnstableRate(score.HitEvents)
|
||||||
}))
|
}), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -24,11 +24,16 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
@ -279,33 +279,32 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Timing Distribution",
|
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
|
||||||
new HitEventTimingDistributionGraph(timedHitEvents)
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}, true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
|
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}, true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new UnstableRate(timedHitEvents)
|
new UnstableRate(timedHitEvents)
|
||||||
}))
|
}), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -24,11 +24,16 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
@ -213,21 +213,21 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
|
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
Height = 250
|
Height = 250
|
||||||
}),
|
}, true),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new StatisticRow
|
new StatisticRow
|
||||||
{
|
{
|
||||||
Columns = new[]
|
Columns = new[]
|
||||||
{
|
{
|
||||||
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||||
{
|
{
|
||||||
new UnstableRate(timedHitEvents)
|
new UnstableRate(timedHitEvents)
|
||||||
}))
|
}), true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -24,11 +24,16 @@
|
|||||||
<string>armv7</string>
|
<string>armv7</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
@ -90,5 +90,100 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCreateNewDifficulty()
|
||||||
|
{
|
||||||
|
string firstDifficultyName = Guid.NewGuid().ToString();
|
||||||
|
string secondDifficultyName = Guid.NewGuid().ToString();
|
||||||
|
|
||||||
|
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
|
||||||
|
AddStep("save beatmap", () => Editor.Save());
|
||||||
|
AddAssert("new beatmap persisted", () =>
|
||||||
|
{
|
||||||
|
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName);
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
|
||||||
|
|
||||||
|
return beatmap != null
|
||||||
|
&& beatmap.DifficultyName == firstDifficultyName
|
||||||
|
&& set != null
|
||||||
|
&& set.PerformRead(s => s.Beatmaps.Single().ID == beatmap.ID);
|
||||||
|
});
|
||||||
|
AddAssert("can save again", () => Editor.Save());
|
||||||
|
|
||||||
|
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
|
||||||
|
AddUntilStep("wait for created", () =>
|
||||||
|
{
|
||||||
|
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||||
|
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName);
|
||||||
|
AddStep("save beatmap", () => Editor.Save());
|
||||||
|
AddAssert("new beatmap persisted", () =>
|
||||||
|
{
|
||||||
|
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName);
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
|
||||||
|
|
||||||
|
return beatmap != null
|
||||||
|
&& beatmap.DifficultyName == secondDifficultyName
|
||||||
|
&& set != null
|
||||||
|
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCreateNewBeatmapFailsWithBlankNamedDifficulties()
|
||||||
|
{
|
||||||
|
Guid setId = Guid.Empty;
|
||||||
|
|
||||||
|
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
|
||||||
|
AddStep("save beatmap", () => Editor.Save());
|
||||||
|
AddAssert("new beatmap persisted", () =>
|
||||||
|
{
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||||
|
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
|
||||||
|
AddAssert("beatmap set unchanged", () =>
|
||||||
|
{
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||||
|
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCreateNewBeatmapFailsWithSameNamedDifficulties()
|
||||||
|
{
|
||||||
|
Guid setId = Guid.Empty;
|
||||||
|
const string duplicate_difficulty_name = "duplicate";
|
||||||
|
|
||||||
|
AddStep("retrieve set ID", () => setId = EditorBeatmap.BeatmapInfo.BeatmapSet!.ID);
|
||||||
|
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||||
|
AddStep("save beatmap", () => Editor.Save());
|
||||||
|
AddAssert("new beatmap persisted", () =>
|
||||||
|
{
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||||
|
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
|
||||||
|
AddUntilStep("wait for created", () =>
|
||||||
|
{
|
||||||
|
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||||
|
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||||
|
AddStep("try to save beatmap", () => Editor.Save());
|
||||||
|
AddAssert("beatmap set not corrupted", () =>
|
||||||
|
{
|
||||||
|
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||||
|
// the difficulty was already created at the point of the switch.
|
||||||
|
// what we want to check is that both difficulties do not use the same file.
|
||||||
|
return set != null && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Files.Count == 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
@ -28,6 +27,7 @@ using osu.Game.Rulesets.Replays.Types;
|
|||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
|
using osu.Game.Tests.Visual.Spectator;
|
||||||
using osu.Game.Tests.Visual.UserInterface;
|
using osu.Game.Tests.Visual.UserInterface;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
@ -36,41 +36,37 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene
|
public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
protected override bool UseOnlineAPI => true;
|
|
||||||
|
|
||||||
private TestRulesetInputManager playbackManager;
|
private TestRulesetInputManager playbackManager;
|
||||||
private TestRulesetInputManager recordingManager;
|
private TestRulesetInputManager recordingManager;
|
||||||
|
|
||||||
private Replay replay;
|
private Replay replay;
|
||||||
|
|
||||||
private TestReplayRecorder recorder;
|
|
||||||
|
|
||||||
private ManualClock manualClock;
|
private ManualClock manualClock;
|
||||||
|
|
||||||
private OsuSpriteText latencyDisplay;
|
private OsuSpriteText latencyDisplay;
|
||||||
|
|
||||||
private TestFramedReplayInputHandler replayHandler;
|
private TestFramedReplayInputHandler replayHandler;
|
||||||
|
|
||||||
[Resolved]
|
|
||||||
private SpectatorClient spectatorClient { get; set; }
|
|
||||||
|
|
||||||
[Cached]
|
|
||||||
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
|
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
{
|
{
|
||||||
AddStep("Reset recorder state", cleanUpState);
|
|
||||||
|
|
||||||
AddStep("Setup containers", () =>
|
AddStep("Setup containers", () =>
|
||||||
{
|
{
|
||||||
replay = new Replay();
|
replay = new Replay();
|
||||||
manualClock = new ManualClock();
|
manualClock = new ManualClock();
|
||||||
|
SpectatorClient spectatorClient;
|
||||||
|
|
||||||
spectatorClient.OnNewFrames += onNewFrames;
|
Child = new DependencyProvidingContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
CachedDependencies = new[]
|
||||||
|
{
|
||||||
|
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
|
||||||
|
(typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>()))
|
||||||
|
},
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
|
spectatorClient,
|
||||||
new GridContainer
|
new GridContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
@ -80,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
{
|
{
|
||||||
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
|
||||||
{
|
{
|
||||||
Recorder = recorder = new TestReplayRecorder
|
Recorder = new TestReplayRecorder
|
||||||
{
|
{
|
||||||
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
|
||||||
},
|
},
|
||||||
@ -140,7 +136,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
latencyDisplay = new OsuSpriteText()
|
latencyDisplay = new OsuSpriteText()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
spectatorClient.OnNewFrames += onNewFrames;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -200,20 +199,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
manualClock.CurrentTime = time.Value;
|
manualClock.CurrentTime = time.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TearDownSteps]
|
|
||||||
public void TearDown()
|
|
||||||
{
|
|
||||||
AddStep("stop recorder", cleanUpState);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cleanUpState()
|
|
||||||
{
|
|
||||||
// Ensure previous recorder is disposed else it may affect the global playing state of `SpectatorClient`.
|
|
||||||
recorder?.RemoveAndDisposeImmediately();
|
|
||||||
recorder = null;
|
|
||||||
spectatorClient.OnNewFrames -= onNewFrames;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
public class TestFramedReplayInputHandler : FramedReplayInputHandler<TestReplayFrame>
|
||||||
{
|
{
|
||||||
public TestFramedReplayInputHandler(Replay replay)
|
public TestFramedReplayInputHandler(Replay replay)
|
||||||
|
@ -6,10 +6,18 @@ using System.Collections.Generic;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics;
|
||||||
|
using osu.Game.Graphics.Sprites;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Difficulty;
|
||||||
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking.Statistics;
|
using osu.Game.Screens.Ranking.Statistics;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Rulesets.Scoring;
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.UI;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
|
|
||||||
@ -41,6 +49,24 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
loadPanel(TestResources.CreateTestScoreInfo());
|
loadPanel(TestResources.CreateTestScoreInfo());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreInRulesetWhereAllStatsRequireHitEvents()
|
||||||
|
{
|
||||||
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetAllStatsRequireHitEvents().RulesetInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreInRulesetWhereNoStatsRequireHitEvents()
|
||||||
|
{
|
||||||
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetNoStatsRequireHitEvents().RulesetInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScoreInMixedRuleset()
|
||||||
|
{
|
||||||
|
loadPanel(TestResources.CreateTestScoreInfo(new TestRulesetMixed().RulesetInfo));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestNullScore()
|
public void TestNullScore()
|
||||||
{
|
{
|
||||||
@ -75,5 +101,134 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
|
|
||||||
return hitEvents;
|
return hitEvents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestRuleset : Ruleset
|
||||||
|
{
|
||||||
|
public override IEnumerable<Mod> GetModsFor(ModType type)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string Description => string.Empty;
|
||||||
|
|
||||||
|
public override string ShortName => string.Empty;
|
||||||
|
|
||||||
|
protected static Drawable CreatePlaceholderStatistic(string message) => new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Masking = true,
|
||||||
|
CornerRadius = 20,
|
||||||
|
Height = 250,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Colour = OsuColour.Gray(0.5f),
|
||||||
|
Alpha = 0.5f
|
||||||
|
},
|
||||||
|
new OsuSpriteText
|
||||||
|
{
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
Anchor = Anchor.CentreLeft,
|
||||||
|
Text = message,
|
||||||
|
Margin = new MarginPadding { Left = 20 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
|
||||||
|
{
|
||||||
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Requiring Hit Events 1",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Requiring Hit Events 2",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRulesetNoStatsRequireHitEvents : TestRuleset
|
||||||
|
{
|
||||||
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Not Requiring Hit Events 1",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Not Requiring Hit Events 2",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestRulesetMixed : TestRuleset
|
||||||
|
{
|
||||||
|
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||||
|
{
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Requiring Hit Events",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new StatisticRow
|
||||||
|
{
|
||||||
|
Columns = new[]
|
||||||
|
{
|
||||||
|
new StatisticItem("Statistic Not Requiring Hit Events",
|
||||||
|
() => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,9 @@ namespace osu.Game.Beatmaps
|
|||||||
new BeatmapModelManager(realm, storage, onlineLookupQueue);
|
new BeatmapModelManager(realm, storage, onlineLookupQueue);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new <see cref="WorkingBeatmap"/>.
|
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
|
||||||
|
/// with a single difficulty which is backed by a <see cref="BeatmapInfo"/> model
|
||||||
|
/// and represented by the returned usable <see cref="WorkingBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user)
|
public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user)
|
||||||
{
|
{
|
||||||
@ -105,6 +107,40 @@ namespace osu.Game.Beatmaps
|
|||||||
return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First()));
|
return imported.PerformRead(s => GetWorkingBeatmap(s.Beatmaps.First()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add a new difficulty to the beatmap set represented by the provided <see cref="BeatmapSetInfo"/>.
|
||||||
|
/// The new difficulty will be backed by a <see cref="BeatmapInfo"/> model
|
||||||
|
/// and represented by the returned <see cref="WorkingBeatmap"/>.
|
||||||
|
/// </summary>
|
||||||
|
public virtual WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo)
|
||||||
|
{
|
||||||
|
// fetch one of the existing difficulties to copy timing points and metadata from,
|
||||||
|
// so that the user doesn't have to fill all of that out again.
|
||||||
|
// this silently assumes that all difficulties have the same timing points and metadata,
|
||||||
|
// but cases where this isn't true seem rather rare / pathological.
|
||||||
|
var referenceBeatmap = GetWorkingBeatmap(beatmapSetInfo.Beatmaps.First());
|
||||||
|
|
||||||
|
var newBeatmapInfo = new BeatmapInfo(rulesetInfo, new BeatmapDifficulty(), referenceBeatmap.Metadata.DeepClone());
|
||||||
|
|
||||||
|
// populate circular beatmap set info <-> beatmap info references manually.
|
||||||
|
// several places like `BeatmapModelManager.Save()` or `GetWorkingBeatmap()`
|
||||||
|
// rely on them being freely traversable in both directions for correct operation.
|
||||||
|
beatmapSetInfo.Beatmaps.Add(newBeatmapInfo);
|
||||||
|
newBeatmapInfo.BeatmapSet = beatmapSetInfo;
|
||||||
|
|
||||||
|
var newBeatmap = new Beatmap { BeatmapInfo = newBeatmapInfo };
|
||||||
|
foreach (var timingPoint in referenceBeatmap.Beatmap.ControlPointInfo.TimingPoints)
|
||||||
|
newBeatmap.ControlPointInfo.Add(timingPoint.Time, timingPoint.DeepClone());
|
||||||
|
|
||||||
|
beatmapModelManager.Save(newBeatmapInfo, newBeatmap);
|
||||||
|
|
||||||
|
workingBeatmapCache.Invalidate(beatmapSetInfo);
|
||||||
|
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add back support for making a copy of another difficulty
|
||||||
|
// (likely via a separate `CopyDifficulty()` method).
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Delete a beatmap difficulty.
|
/// Delete a beatmap difficulty.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -7,6 +7,7 @@ using Newtonsoft.Json;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Utils;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps
|
|||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
[Serializable]
|
[Serializable]
|
||||||
[MapTo("BeatmapMetadata")]
|
[MapTo("BeatmapMetadata")]
|
||||||
public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo
|
public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo, IDeepCloneable<BeatmapMetadata>
|
||||||
{
|
{
|
||||||
public string Title { get; set; } = string.Empty;
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
@ -57,5 +58,18 @@ namespace osu.Game.Beatmaps
|
|||||||
IUser IBeatmapMetadataInfo.Author => Author;
|
IUser IBeatmapMetadataInfo.Author => Author;
|
||||||
|
|
||||||
public override string ToString() => this.GetDisplayTitle();
|
public override string ToString() => this.GetDisplayTitle();
|
||||||
|
|
||||||
|
public BeatmapMetadata DeepClone() => new BeatmapMetadata(Author.DeepClone())
|
||||||
|
{
|
||||||
|
Title = Title,
|
||||||
|
TitleUnicode = TitleUnicode,
|
||||||
|
Artist = Artist,
|
||||||
|
ArtistUnicode = ArtistUnicode,
|
||||||
|
Source = Source,
|
||||||
|
Tags = Tags,
|
||||||
|
PreviewTime = PreviewTime,
|
||||||
|
AudioFile = AudioFile,
|
||||||
|
BackgroundFile = BackgroundFile
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,10 +46,9 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
|
||||||
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
|
||||||
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
|
||||||
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
|
public void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
|
||||||
{
|
{
|
||||||
var setInfo = beatmapInfo.BeatmapSet;
|
var setInfo = beatmapInfo.BeatmapSet;
|
||||||
|
|
||||||
Debug.Assert(setInfo != null);
|
Debug.Assert(setInfo != null);
|
||||||
|
|
||||||
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
|
||||||
@ -72,6 +71,12 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
|
||||||
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
|
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase));
|
||||||
|
string targetFilename = getFilename(beatmapInfo);
|
||||||
|
|
||||||
|
// ensure that two difficulties from the set don't point at the same beatmap file.
|
||||||
|
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
|
||||||
|
|
||||||
if (existingFileInfo != null)
|
if (existingFileInfo != null)
|
||||||
DeleteFile(setInfo, existingFileInfo);
|
DeleteFile(setInfo, existingFileInfo);
|
||||||
|
|
||||||
@ -103,9 +108,9 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public void Update(BeatmapSetInfo item)
|
public void Update(BeatmapSetInfo item)
|
||||||
{
|
{
|
||||||
Realm.Write(realm =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var existing = realm.Find<BeatmapSetInfo>(item.ID);
|
var existing = r.Find<BeatmapSetInfo>(item.ID);
|
||||||
item.CopyChangesToRealm(existing);
|
item.CopyChangesToRealm(existing);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,16 @@ namespace osu.Game.Database
|
|||||||
if (existing != null)
|
if (existing != null)
|
||||||
copyChangesToRealm(beatmap, existing);
|
copyChangesToRealm(beatmap, existing);
|
||||||
else
|
else
|
||||||
d.Beatmaps.Add(beatmap);
|
{
|
||||||
|
var newBeatmap = new BeatmapInfo
|
||||||
|
{
|
||||||
|
ID = beatmap.ID,
|
||||||
|
BeatmapSet = d,
|
||||||
|
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)
|
||||||
|
};
|
||||||
|
d.Beatmaps.Add(newBeatmap);
|
||||||
|
copyChangesToRealm(beatmap, newBeatmap);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,6 +117,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
{
|
{
|
||||||
NormalText = new OsuSpriteText
|
NormalText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
|
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Font = OsuFont.GetFont(size: text_size),
|
Font = OsuFont.GetFont(size: text_size),
|
||||||
@ -124,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
},
|
},
|
||||||
BoldText = new OsuSpriteText
|
BoldText = new OsuSpriteText
|
||||||
{
|
{
|
||||||
AlwaysPresent = true,
|
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
|
||||||
Alpha = 0,
|
Alpha = 0,
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
using osu.Game.Utils;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
namespace osu.Game.Models
|
namespace osu.Game.Models
|
||||||
{
|
{
|
||||||
public class RealmUser : EmbeddedObject, IUser, IEquatable<RealmUser>
|
public class RealmUser : EmbeddedObject, IUser, IEquatable<RealmUser>, IDeepCloneable<RealmUser>
|
||||||
{
|
{
|
||||||
public int OnlineID { get; set; } = 1;
|
public int OnlineID { get; set; } = 1;
|
||||||
|
|
||||||
@ -22,5 +24,7 @@ namespace osu.Game.Models
|
|||||||
|
|
||||||
return OnlineID == other.OnlineID && Username == other.Username;
|
return OnlineID == other.OnlineID && Username == other.Username;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RealmUser DeepClone() => (RealmUser)this.Detach().MemberwiseClone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,9 @@ namespace osu.Game.Screens.Edit
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private BeatmapManager beatmapManager { get; set; }
|
private BeatmapManager beatmapManager { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private RulesetStore rulesets { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private Storage storage { get; set; }
|
private Storage storage { get; set; }
|
||||||
|
|
||||||
@ -375,21 +378,34 @@ namespace osu.Game.Screens.Edit
|
|||||||
Clipboard.Content.Value = state.ClipboardContent;
|
Clipboard.Content.Value = state.ClipboardContent;
|
||||||
});
|
});
|
||||||
|
|
||||||
protected void Save()
|
/// <summary>
|
||||||
|
/// Saves the currently edited beatmap.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Whether the save was successful.</returns>
|
||||||
|
protected bool Save()
|
||||||
{
|
{
|
||||||
if (!canSave)
|
if (!canSave)
|
||||||
{
|
{
|
||||||
notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" });
|
notifications?.Post(new SimpleErrorNotification { Text = "Saving is not supported for this ruleset yet, sorry!" });
|
||||||
return;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// save the loaded beatmap's data stream.
|
||||||
|
beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// can fail e.g. due to duplicated difficulty names.
|
||||||
|
Logger.Error(ex, ex.Message);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// no longer new after first user-triggered save.
|
// no longer new after first user-triggered save.
|
||||||
isNewBeatmap = false;
|
isNewBeatmap = false;
|
||||||
|
|
||||||
// save the loaded beatmap's data stream.
|
|
||||||
beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
|
|
||||||
|
|
||||||
updateLastSavedHash();
|
updateLastSavedHash();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
@ -798,7 +814,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
var fileMenuItems = new List<MenuItem>
|
var fileMenuItems = new List<MenuItem>
|
||||||
{
|
{
|
||||||
new EditorMenuItem("Save", MenuItemType.Standard, Save)
|
new EditorMenuItem("Save", MenuItemType.Standard, () => Save())
|
||||||
};
|
};
|
||||||
|
|
||||||
if (RuntimeInfo.IsDesktop)
|
if (RuntimeInfo.IsDesktop)
|
||||||
@ -806,6 +822,29 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
|
fileMenuItems.Add(createDifficultyCreationMenu());
|
||||||
|
fileMenuItems.Add(createDifficultySwitchMenu());
|
||||||
|
|
||||||
|
fileMenuItems.Add(new EditorMenuItemSpacer());
|
||||||
|
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
||||||
|
return fileMenuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EditorMenuItem createDifficultyCreationMenu()
|
||||||
|
{
|
||||||
|
var rulesetItems = new List<MenuItem>();
|
||||||
|
|
||||||
|
foreach (var ruleset in rulesets.AvailableRulesets)
|
||||||
|
rulesetItems.Add(new EditorMenuItem(ruleset.Name, MenuItemType.Standard, () => CreateNewDifficulty(ruleset)));
|
||||||
|
|
||||||
|
return new EditorMenuItem("Create new difficulty") { Items = rulesetItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CreateNewDifficulty(RulesetInfo rulesetInfo)
|
||||||
|
=> loader?.ScheduleSwitchToNewDifficulty(editorBeatmap.BeatmapInfo.BeatmapSet, rulesetInfo, GetState());
|
||||||
|
|
||||||
|
private EditorMenuItem createDifficultySwitchMenu()
|
||||||
|
{
|
||||||
var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet;
|
var beatmapSet = playableBeatmap.BeatmapInfo.BeatmapSet;
|
||||||
|
|
||||||
Debug.Assert(beatmapSet != null);
|
Debug.Assert(beatmapSet != null);
|
||||||
@ -818,23 +857,16 @@ namespace osu.Game.Screens.Edit
|
|||||||
difficultyItems.Add(new EditorMenuItemSpacer());
|
difficultyItems.Add(new EditorMenuItemSpacer());
|
||||||
|
|
||||||
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
|
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
|
||||||
difficultyItems.Add(createDifficultyMenuItem(beatmap));
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems });
|
|
||||||
|
|
||||||
fileMenuItems.Add(new EditorMenuItemSpacer());
|
|
||||||
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
|
|
||||||
return fileMenuItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
|
|
||||||
{
|
{
|
||||||
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
|
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
|
||||||
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
|
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
|
return new EditorMenuItem("Change difficulty") { Items = difficultyItems };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap));
|
||||||
|
|
||||||
private void cancelExit()
|
private void cancelExit()
|
||||||
{
|
{
|
||||||
|
@ -6,10 +6,12 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
@ -78,7 +80,26 @@ namespace osu.Game.Screens.Edit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ScheduleDifficultySwitch(BeatmapInfo nextBeatmap, EditorState editorState)
|
public void ScheduleSwitchToNewDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo, EditorState editorState)
|
||||||
|
=> scheduleDifficultySwitch(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return beatmapManager.CreateNewBlankDifficulty(beatmapSetInfo, rulesetInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// if the beatmap creation fails (e.g. due to duplicated difficulty names),
|
||||||
|
// bring the user back to the previous beatmap as a best-effort.
|
||||||
|
Logger.Error(ex, ex.Message);
|
||||||
|
return Beatmap.Value;
|
||||||
|
}
|
||||||
|
}, editorState);
|
||||||
|
|
||||||
|
public void ScheduleSwitchToExistingDifficulty(BeatmapInfo beatmapInfo, EditorState editorState)
|
||||||
|
=> scheduleDifficultySwitch(() => beatmapManager.GetWorkingBeatmap(beatmapInfo), editorState);
|
||||||
|
|
||||||
|
private void scheduleDifficultySwitch(Func<WorkingBeatmap> nextBeatmap, EditorState editorState)
|
||||||
{
|
{
|
||||||
scheduledDifficultySwitch?.Cancel();
|
scheduledDifficultySwitch?.Cancel();
|
||||||
ValidForResume = true;
|
ValidForResume = true;
|
||||||
@ -87,7 +108,7 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
scheduledDifficultySwitch = Schedule(() =>
|
scheduledDifficultySwitch = Schedule(() =>
|
||||||
{
|
{
|
||||||
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap);
|
Beatmap.Value = nextBeatmap.Invoke();
|
||||||
state = editorState;
|
state = editorState;
|
||||||
|
|
||||||
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.
|
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.
|
||||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Margin = new MarginPadding { Top = 15 },
|
Margin = new MarginPadding { Top = 15 },
|
||||||
Child = item.Content
|
Child = item.CreateContent()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -18,25 +19,38 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
public readonly string Name;
|
public readonly string Name;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="Drawable"/> content to be displayed.
|
/// A function returning the <see cref="Drawable"/> content to be displayed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Drawable Content;
|
public readonly Func<Drawable> CreateContent;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="Dimension"/> of this row. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.
|
/// The <see cref="Dimension"/> of this row. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly Dimension Dimension;
|
public readonly Dimension Dimension;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this item requires hit events. If true, <see cref="CreateContent"/> will not be called if no hit events are available.
|
||||||
|
/// </summary>
|
||||||
|
public readonly bool RequiresHitEvents;
|
||||||
|
|
||||||
|
[Obsolete("Use constructor which takes creation function instead.")] // Can be removed 20220803.
|
||||||
|
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)
|
||||||
|
: this(name, () => content, true, dimension)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
|
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the item. Can be <see cref="string.Empty"/> to hide the item header.</param>
|
/// <param name="name">The name of the item. Can be <see cref="string.Empty"/> to hide the item header.</param>
|
||||||
/// <param name="content">The <see cref="Drawable"/> content to be displayed.</param>
|
/// <param name="createContent">A function returning the <see cref="Drawable"/> content to be displayed.</param>
|
||||||
|
/// <param name="requiresHitEvents">Whether this item requires hit events. If true, <see cref="CreateContent"/> will not be called if no hit events are available.</param>
|
||||||
/// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param>
|
/// <param name="dimension">The <see cref="Dimension"/> of this item. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.</param>
|
||||||
public StatisticItem([NotNull] string name, [NotNull] Drawable content, [CanBeNull] Dimension dimension = null)
|
public StatisticItem([NotNull] string name, [NotNull] Func<Drawable> createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
|
||||||
{
|
{
|
||||||
Name = name;
|
Name = name;
|
||||||
Content = content;
|
RequiresHitEvents = requiresHitEvents;
|
||||||
|
CreateContent = createContent;
|
||||||
Dimension = dimension;
|
Dimension = dimension;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -10,6 +11,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online.Placeholders;
|
using osu.Game.Online.Placeholders;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
@ -74,26 +76,6 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
if (newScore == null)
|
if (newScore == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (newScore.HitEvents.Count == 0)
|
|
||||||
{
|
|
||||||
content.Add(new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new MessagePlaceholder("Extended statistics are only available after watching a replay!"),
|
|
||||||
new ReplayDownloadButton(newScore)
|
|
||||||
{
|
|
||||||
Scale = new Vector2(1.5f),
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
spinner.Show();
|
spinner.Show();
|
||||||
|
|
||||||
var localCancellationSource = loadCancellation = new CancellationTokenSource();
|
var localCancellationSource = loadCancellation = new CancellationTokenSource();
|
||||||
@ -105,40 +87,116 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
|
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
|
||||||
}, loadCancellation.Token).ContinueWith(t => Schedule(() =>
|
}, loadCancellation.Token).ContinueWith(t => Schedule(() =>
|
||||||
{
|
{
|
||||||
var rows = new FillFlowContainer
|
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
|
||||||
|
Container<Drawable> container;
|
||||||
|
|
||||||
|
var statisticRows = newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap);
|
||||||
|
|
||||||
|
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
container = new FillFlowContainer
|
||||||
AutoSizeAxes = Axes.Y,
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
Direction = FillDirection.Vertical,
|
Direction = FillDirection.Vertical,
|
||||||
Spacing = new Vector2(30, 15),
|
Children = new Drawable[]
|
||||||
Alpha = 0
|
{
|
||||||
|
new MessagePlaceholder("Extended statistics are only available after watching a replay!"),
|
||||||
|
new ReplayDownloadButton(newScore)
|
||||||
|
{
|
||||||
|
Scale = new Vector2(1.5f),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FillFlowContainer rows;
|
||||||
|
container = new OsuScrollContainer(Direction.Vertical)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
Alpha = 0,
|
||||||
|
Children = new[]
|
||||||
|
{
|
||||||
|
rows = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Spacing = new Vector2(30, 15)
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap))
|
bool anyRequiredHitEvents = false;
|
||||||
|
|
||||||
|
foreach (var row in statisticRows)
|
||||||
{
|
{
|
||||||
|
var columns = row.Columns;
|
||||||
|
|
||||||
|
if (columns.Length == 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var columnContent = new List<Drawable>();
|
||||||
|
var dimensions = new List<Dimension>();
|
||||||
|
|
||||||
|
foreach (var col in columns)
|
||||||
|
{
|
||||||
|
if (!hitEventsAvailable && col.RequiresHitEvents)
|
||||||
|
{
|
||||||
|
anyRequiredHitEvents = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
columnContent.Add(new StatisticContainer(col)
|
||||||
|
{
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
});
|
||||||
|
|
||||||
|
dimensions.Add(col.Dimension ?? new Dimension());
|
||||||
|
}
|
||||||
|
|
||||||
rows.Add(new GridContainer
|
rows.Add(new GridContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.TopCentre,
|
Anchor = Anchor.TopCentre,
|
||||||
Origin = Anchor.TopCentre,
|
Origin = Anchor.TopCentre,
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Content = new[]
|
Content = new[] { columnContent.ToArray() },
|
||||||
{
|
ColumnDimensions = dimensions.ToArray(),
|
||||||
row.Columns?.Select(c => new StatisticContainer(c)
|
|
||||||
{
|
|
||||||
Anchor = Anchor.Centre,
|
|
||||||
Origin = Anchor.Centre,
|
|
||||||
}).Cast<Drawable>().ToArray()
|
|
||||||
},
|
|
||||||
ColumnDimensions = Enumerable.Range(0, row.Columns?.Length ?? 0)
|
|
||||||
.Select(i => row.Columns[i].Dimension ?? new Dimension()).ToArray(),
|
|
||||||
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadComponentAsync(rows, d =>
|
if (anyRequiredHitEvents)
|
||||||
|
{
|
||||||
|
rows.Add(new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new MessagePlaceholder("More statistics available after watching a replay!"),
|
||||||
|
new ReplayDownloadButton(newScore)
|
||||||
|
{
|
||||||
|
Scale = new Vector2(1.5f),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoadComponentAsync(container, d =>
|
||||||
{
|
{
|
||||||
if (!Score.Value.Equals(newScore))
|
if (!Score.Value.Equals(newScore))
|
||||||
return;
|
return;
|
||||||
@ -149,7 +207,6 @@ namespace osu.Game.Screens.Ranking.Statistics
|
|||||||
}, localCancellationSource.Token);
|
}, localCancellationSource.Token);
|
||||||
}), localCancellationSource.Token);
|
}), localCancellationSource.Token);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
|
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
public new void Redo() => base.Redo();
|
public new void Redo() => base.Redo();
|
||||||
|
|
||||||
public new void Save() => base.Save();
|
public new bool Save() => base.Save();
|
||||||
|
|
||||||
public new void Cut() => base.Cut();
|
public new void Cut() => base.Cut();
|
||||||
|
|
||||||
@ -107,6 +107,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
|
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
|
||||||
|
|
||||||
|
public new void CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);
|
||||||
|
|
||||||
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
||||||
|
|
||||||
public TestEditor(EditorLoader loader = null)
|
public TestEditor(EditorLoader loader = null)
|
||||||
@ -134,6 +136,12 @@ namespace osu.Game.Tests.Visual
|
|||||||
return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host);
|
return new TestWorkingBeatmapCache(this, audioManager, resources, storage, defaultBeatmap, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override WorkingBeatmap CreateNewBlankDifficulty(BeatmapSetInfo beatmapSetInfo, RulesetInfo rulesetInfo)
|
||||||
|
{
|
||||||
|
// don't actually care about properly creating a difficulty for this context.
|
||||||
|
return TestBeatmap;
|
||||||
|
}
|
||||||
|
|
||||||
private class TestWorkingBeatmapCache : WorkingBeatmapCache
|
private class TestWorkingBeatmapCache : WorkingBeatmapCache
|
||||||
{
|
{
|
||||||
private readonly TestBeatmapManager testBeatmapManager;
|
private readonly TestBeatmapManager testBeatmapManager;
|
||||||
|
@ -36,7 +36,7 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Realm" Version="10.8.0" />
|
<PackageReference Include="Realm" Version="10.8.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.128.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.204.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.13.0" />
|
<PackageReference Include="Sentry" Version="3.13.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
<PackageReference Include="SharpCompress" Version="0.30.1" />
|
||||||
|
@ -60,7 +60,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.128.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.204.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
|
||||||
@ -83,7 +83,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2022.128.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2022.204.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.30.0" />
|
<PackageReference Include="SharpCompress" Version="0.30.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||||
|
@ -40,11 +40,16 @@
|
|||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We don't really use the microphone.</string>
|
<string>We don't really use the microphone.</string>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
</array>
|
</array>
|
||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/AppIcon.appiconset</string>
|
<string>Assets.xcassets/AppIcon.appiconset</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user