1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 07:22:54 +08:00

Merge branch 'fix-broken-test-scene' into spectator-state-rework

This commit is contained in:
Dan Balasescu 2022-02-04 22:42:10 +09:00
commit 09728a29ed
29 changed files with 701 additions and 215 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<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 Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -27,7 +27,7 @@ namespace osu.Android
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
});
}
}

View File

@ -1,6 +1,7 @@
// 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.IO;
using System.Linq;
@ -8,16 +9,18 @@ using System.Threading.Tasks;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Net;
using Android.Graphics;
using Android.OS;
using Android.Provider;
using Android.Views;
using osu.Framework.Android;
using osu.Game.Database;
using Debug = System.Diagnostics.Debug;
using Uri = Android.Net.Uri;
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 = ".*\\\\.osk", 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" };
/// <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;
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)
handleIntent(Intent);
Debug.Assert(Window != null);
Window.AddFlags(WindowManagerFlags.Fullscreen);
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);
@ -104,7 +125,7 @@ namespace osu.Android
cursor.MoveToFirst();
var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
int filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName);
string filename = cursor.GetString(filenameColumn);
// SharpCompress requires archive streams to be seekable, which the stream opened by

View File

@ -24,11 +24,16 @@
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -24,11 +24,16 @@
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -370,21 +370,21 @@ namespace osu.Game.Rulesets.Mania
{
Columns = new[]
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(score.HitEvents)
}))
}), true)
}
}
};

View File

@ -24,11 +24,16 @@
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -279,33 +279,32 @@ namespace osu.Game.Rulesets.Osu
{
Columns = new[]
{
new StatisticItem("Timing Distribution",
new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}), true)
}
}
};

View File

@ -24,11 +24,16 @@
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -213,21 +213,21 @@ namespace osu.Game.Rulesets.Taiko
{
Columns = new[]
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents)
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}, true),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[]
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new UnstableRate(timedHitEvents)
}))
}), true)
}
}
};

View File

@ -24,11 +24,16 @@
<string>armv7</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>

View File

@ -90,5 +90,100 @@ namespace osu.Game.Tests.Visual.Editing
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);
});
}
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -28,6 +27,7 @@ using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Tests.Visual.UserInterface;
using osuTK;
using osuTK.Graphics;
@ -36,111 +36,110 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene
{
protected override bool UseOnlineAPI => true;
private TestRulesetInputManager playbackManager;
private TestRulesetInputManager recordingManager;
private Replay replay;
private TestReplayRecorder recorder;
private ManualClock manualClock;
private OsuSpriteText latencyDisplay;
private TestFramedReplayInputHandler replayHandler;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
[Cached]
private GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>());
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Reset recorder state", cleanUpState);
AddStep("Setup containers", () =>
{
replay = new Replay();
manualClock = new ManualClock();
SpectatorClient spectatorClient;
spectatorClient.OnNewFrames += onNewFrames;
Children = new Drawable[]
Child = new DependencyProvidingContainer
{
new GridContainer
RelativeSizeAxes = Axes.Both,
CachedDependencies = new[]
{
RelativeSizeAxes = Axes.Both,
Content = new[]
(typeof(SpectatorClient), (object)(spectatorClient = new TestSpectatorClient())),
(typeof(GameplayState), new GameplayState(new Beatmap(), new OsuRuleset(), Array.Empty<Mod>()))
},
Children = new Drawable[]
{
spectatorClient,
new GridContainer
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
Content = new[]
{
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
new Drawable[]
{
Recorder = recorder = new TestReplayRecorder
recordingManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Recorder = new TestReplayRecorder
{
new Box
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Sending",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
new Box
{
Colour = Color4.Brown,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Sending",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
},
new Drawable[]
{
Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
playbackManager = new TestRulesetInputManager(TestSceneModSettings.CreateTestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Clock = new FramedClock(manualClock),
ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay)
{
new Box
GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos),
},
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Receiving",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
new Box
{
Colour = Color4.DarkBlue,
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Text = "Receiving",
Scale = new Vector2(3),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
new TestInputConsumer()
}
},
}
}
}
}
},
latencyDisplay = new OsuSpriteText()
},
latencyDisplay = new OsuSpriteText()
}
};
spectatorClient.OnNewFrames += onNewFrames;
});
}
@ -200,20 +199,6 @@ namespace osu.Game.Tests.Visual.Gameplay
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 TestFramedReplayInputHandler(Replay replay)

View File

@ -6,10 +6,18 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
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.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Resources;
using osuTK;
@ -41,6 +49,24 @@ namespace osu.Game.Tests.Visual.Ranking
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]
public void TestNullScore()
{
@ -75,5 +101,134 @@ namespace osu.Game.Tests.Visual.Ranking
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"))
}
}
};
}
}
}
}

View File

@ -73,7 +73,9 @@ namespace osu.Game.Beatmaps
new BeatmapModelManager(realm, storage, onlineLookupQueue);
/// <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>
public WorkingBeatmap CreateNew(RulesetInfo ruleset, APIUser user)
{
@ -105,6 +107,40 @@ namespace osu.Game.Beatmaps
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>
/// Delete a beatmap difficulty.
/// </summary>

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using osu.Framework.Testing;
using osu.Game.Models;
using osu.Game.Users;
using osu.Game.Utils;
using Realms;
#nullable enable
@ -16,7 +17,7 @@ namespace osu.Game.Beatmaps
[ExcludeFromDynamicCompile]
[Serializable]
[MapTo("BeatmapMetadata")]
public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo
public class BeatmapMetadata : RealmObject, IBeatmapMetadataInfo, IDeepCloneable<BeatmapMetadata>
{
public string Title { get; set; } = string.Empty;
@ -57,5 +58,18 @@ namespace osu.Game.Beatmaps
IUser IBeatmapMetadataInfo.Author => Author;
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
};
}
}

View File

@ -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="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>
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;
Debug.Assert(setInfo != null);
// 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.
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)
DeleteFile(setInfo, existingFileInfo);
@ -103,9 +108,9 @@ namespace osu.Game.Beatmaps
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);
});
}

View File

@ -58,7 +58,16 @@ namespace osu.Game.Database
if (existing != null)
copyChangesToRealm(beatmap, existing);
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);
}
}
});

View File

@ -117,6 +117,7 @@ namespace osu.Game.Graphics.UserInterface
{
NormalText = new OsuSpriteText
{
AlwaysPresent = true, // ensures that the menu item does not change width when switching between normal and bold text.
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: text_size),
@ -124,7 +125,7 @@ namespace osu.Game.Graphics.UserInterface
},
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,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,

View File

@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Database;
using osu.Game.Users;
using osu.Game.Utils;
using Realms;
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;
@ -22,5 +24,7 @@ namespace osu.Game.Models
return OnlineID == other.OnlineID && Username == other.Username;
}
public RealmUser DeepClone() => (RealmUser)this.Detach().MemberwiseClone();
}
}

View File

@ -77,6 +77,9 @@ namespace osu.Game.Screens.Edit
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private Storage storage { get; set; }
@ -375,21 +378,34 @@ namespace osu.Game.Screens.Edit
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)
{
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.
isNewBeatmap = false;
// save the loaded beatmap's data stream.
beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin);
updateLastSavedHash();
return true;
}
protected override void Update()
@ -798,7 +814,7 @@ namespace osu.Game.Screens.Edit
{
var fileMenuItems = new List<MenuItem>
{
new EditorMenuItem("Save", MenuItemType.Standard, Save)
new EditorMenuItem("Save", MenuItemType.Standard, () => Save())
};
if (RuntimeInfo.IsDesktop)
@ -806,6 +822,29 @@ namespace osu.Game.Screens.Edit
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;
Debug.Assert(beatmapSet != null);
@ -818,23 +857,16 @@ namespace osu.Game.Screens.Edit
difficultyItems.Add(new EditorMenuItemSpacer());
foreach (var beatmap in rulesetBeatmaps.OrderBy(b => b.StarRating))
difficultyItems.Add(createDifficultyMenuItem(beatmap));
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmap);
difficultyItems.Add(new DifficultyMenuItem(beatmap, isCurrentDifficulty, SwitchToDifficulty));
}
}
fileMenuItems.Add(new EditorMenuItem("Change difficulty") { Items = difficultyItems });
fileMenuItems.Add(new EditorMenuItemSpacer());
fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit));
return fileMenuItems;
return new EditorMenuItem("Change difficulty") { Items = difficultyItems };
}
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
}
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleDifficultySwitch(nextBeatmap, GetState(nextBeatmap));
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap));
private void cancelExit()
{

View File

@ -6,10 +6,12 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Menu;
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();
ValidForResume = true;
@ -87,7 +108,7 @@ namespace osu.Game.Screens.Edit
scheduledDifficultySwitch = Schedule(() =>
{
Beatmap.Value = beatmapManager.GetWorkingBeatmap(nextBeatmap);
Beatmap.Value = nextBeatmap.Invoke();
state = editorState;
// This screen is a weird exception to the rule that nothing after song select changes the global beatmap.

View File

@ -43,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = 15 },
Child = item.Content
Child = item.CreateContent()
}
},
},

View File

@ -1,6 +1,7 @@
// 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 JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -18,25 +19,38 @@ namespace osu.Game.Screens.Ranking.Statistics
public readonly string Name;
/// <summary>
/// The <see cref="Drawable"/> content to be displayed.
/// A function returning the <see cref="Drawable"/> content to be displayed.
/// </summary>
public readonly Drawable Content;
public readonly Func<Drawable> CreateContent;
/// <summary>
/// The <see cref="Dimension"/> of this row. This can be thought of as the column dimension of an encompassing <see cref="GridContainer"/>.
/// </summary>
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>
/// Creates a new <see cref="StatisticItem"/>, to be displayed inside a <see cref="StatisticRow"/> in the results screen.
/// </summary>
/// <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>
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;
Content = content;
RequiresHitEvents = requiresHitEvents;
CreateContent = createContent;
Dimension = dimension;
}
}

View File

@ -1,6 +1,7 @@
// 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.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -10,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Placeholders;
using osu.Game.Scoring;
@ -74,81 +76,136 @@ namespace osu.Game.Screens.Ranking.Statistics
if (newScore == null)
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();
IBeatmap playableBeatmap = null;
var localCancellationSource = loadCancellation = new CancellationTokenSource();
IBeatmap playableBeatmap = null;
// Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
Task.Run(() =>
// Todo: The placement of this is temporary. Eventually we'll both generate the playable beatmap _and_ run through it in a background task to generate the hit events.
Task.Run(() =>
{
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
}, loadCancellation.Token).ContinueWith(t => Schedule(() =>
{
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))
{
playableBeatmap = beatmapManager.GetWorkingBeatmap(newScore.BeatmapInfo).GetPlayableBeatmap(newScore.Ruleset, newScore.Mods);
}, loadCancellation.Token).ContinueWith(t => Schedule(() =>
{
var rows = new FillFlowContainer
container = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 15),
Alpha = 0
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
{
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
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
{
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(),
Content = new[] { columnContent.ToArray() },
ColumnDimensions = dimensions.ToArray(),
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
});
}
LoadComponentAsync(rows, d =>
if (anyRequiredHitEvents)
{
if (!Score.Value.Equals(newScore))
return;
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,
},
}
});
}
}
spinner.Hide();
content.Add(d);
d.FadeIn(250, Easing.OutQuint);
}, localCancellationSource.Token);
}), localCancellationSource.Token);
}
LoadComponentAsync(container, d =>
{
if (!Score.Value.Equals(newScore))
return;
spinner.Hide();
content.Add(d);
d.FadeIn(250, Easing.OutQuint);
}, localCancellationSource.Token);
}), localCancellationSource.Token);
}
protected override bool OnClick(ClickEvent e)

View File

@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual
public new void Redo() => base.Redo();
public new void Save() => base.Save();
public new bool Save() => base.Save();
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 CreateNewDifficulty(RulesetInfo rulesetInfo) => base.CreateNewDifficulty(rulesetInfo);
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
public TestEditor(EditorLoader loader = null)
@ -134,6 +136,12 @@ namespace osu.Game.Tests.Visual
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 readonly TestBeatmapManager testBeatmapManager;

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="Sentry" Version="3.13.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -60,7 +60,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<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" />
</ItemGroup>
<!-- 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.Core" Version="2.2.6" />
<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="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />

View File

@ -40,11 +40,16 @@
<key>NSMicrophoneUsageDescription</key>
<string>We don&apos;t really use the microphone.</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>