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

Merge branch 'master' into add-last-edit-time

This commit is contained in:
Dean Herbert 2023-06-06 15:07:20 +09:00
commit 3a50e0fb2f
41 changed files with 443 additions and 441 deletions

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.521.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.531.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -21,18 +21,29 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
SetDefault(ManiaRulesetSetting.ScrollSpeed, 8, 1, 40);
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
#pragma warning disable CS0618
// Although obsolete, this is still required to populate the bindable from the database in case migration is required.
SetDefault<double?>(ManiaRulesetSetting.ScrollTime, null);
if (Get<double?>(ManiaRulesetSetting.ScrollTime) is double scrollTime)
{
SetValue(ManiaRulesetSetting.ScrollSpeed, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime));
SetValue<double?>(ManiaRulesetSetting.ScrollTime, null);
}
#pragma warning restore CS0618
}
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
scrollTime => new SettingDescription(
rawValue: scrollTime,
new TrackedSetting<int>(ManiaRulesetSetting.ScrollSpeed,
speed => new SettingDescription(
rawValue: speed,
name: RulesetSettingsStrings.ScrollSpeed,
value: RulesetSettingsStrings.ScrollSpeedTooltip(scrollTime, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime))
value: RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(speed), speed)
)
)
};
@ -40,7 +51,9 @@ namespace osu.Game.Rulesets.Mania.Configuration
public enum ManiaRulesetSetting
{
[Obsolete("Use ScrollSpeed instead.")] // Can be removed 2023-11-30
ScrollTime,
ScrollSpeed,
ScrollDirection,
TimingBasedNoteColouring
}

View File

@ -389,41 +389,23 @@ namespace osu.Game.Rulesets.Mania
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(score.HitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)
}), true)
}
}
new AverageHitError(score.HitEvents),
new UnstableRate(score.HitEvents)
}), true)
};
public override IRulesetFilterCriteria CreateRulesetFilterCriteria()

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
@ -34,10 +33,10 @@ namespace osu.Game.Rulesets.Mania
LabelText = RulesetSettingsStrings.ScrollingDirection,
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider<double, ManiaScrollSlider>
new SettingsSlider<int, ManiaScrollSlider>
{
LabelText = RulesetSettingsStrings.ScrollSpeed,
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
Current = config.GetBindable<int>(ManiaRulesetSetting.ScrollSpeed),
KeyboardStep = 5
},
new SettingsCheckbox
@ -48,9 +47,9 @@ namespace osu.Game.Rulesets.Mania
};
}
private partial class ManiaScrollSlider : RoundedSliderBar<double>
private partial class ManiaScrollSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(DrawableManiaRuleset.ComputeScrollTime(Current.Value), Current.Value);
}
}
}

View File

@ -245,7 +245,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// As the note is being held, adjust the size of the sizing container. This has two effects:
// 1. The contained masking container will mask the body and ticks.
// 2. The head note will move along with the new "head position" in the container.
if (Head.IsHit && releaseTime == null && DrawHeight > 0)
//
// As per stable, this should not apply for early hits, waiting until the object starts to touch the
// judgement area first.
if (Head.IsHit && releaseTime == null && DrawHeight > 0 && Time.Current >= HitObject.StartTime)
{
// How far past the hit target this hold note is.
float yOffset = Direction.Value == ScrollingDirection.Up ? -Y : Y;

View File

@ -33,12 +33,12 @@ namespace osu.Game.Rulesets.Mania.UI
public partial class DrawableManiaRuleset : DrawableScrollingRuleset<ManiaHitObject>
{
/// <summary>
/// The minimum time range. This occurs at a <see cref="relativeTimeRange"/> of 40.
/// The minimum time range. This occurs at a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 40.
/// </summary>
public const double MIN_TIME_RANGE = 290;
/// <summary>
/// The maximum time range. This occurs at a <see cref="relativeTimeRange"/> of 1.
/// The maximum time range. This occurs with a <see cref="ManiaRulesetSetting.ScrollSpeed"/> of 1.
/// </summary>
public const double MAX_TIME_RANGE = 11485;
@ -69,7 +69,8 @@ namespace osu.Game.Rulesets.Mania.UI
protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod;
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
private readonly BindableDouble configTimeRange = new BindableDouble();
private readonly BindableInt configScrollSpeed = new BindableInt();
private double smoothTimeRange;
// Stores the current speed adjustment active in gameplay.
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
@ -78,6 +79,9 @@ namespace osu.Game.Rulesets.Mania.UI
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
TimeRange.MinValue = 1;
TimeRange.MaxValue = MAX_TIME_RANGE;
}
[BackgroundDependencyLoader]
@ -104,30 +108,28 @@ namespace osu.Game.Rulesets.Mania.UI
Config.BindWith(ManiaRulesetSetting.ScrollDirection, configDirection);
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
Config.BindWith(ManiaRulesetSetting.ScrollTime, configTimeRange);
TimeRange.MinValue = configTimeRange.MinValue;
TimeRange.MaxValue = configTimeRange.MaxValue;
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
}
protected override void AdjustScrollSpeed(int amount)
{
this.TransformTo(nameof(relativeTimeRange), relativeTimeRange + amount, 200, Easing.OutQuint);
}
private double relativeTimeRange
{
get => MAX_TIME_RANGE / configTimeRange.Value;
set => configTimeRange.Value = MAX_TIME_RANGE / value;
}
protected override void AdjustScrollSpeed(int amount) => configScrollSpeed.Value += amount;
protected override void Update()
{
base.Update();
updateTimeRange();
}
private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
private void updateTimeRange() => TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
/// <summary>
/// Computes a scroll time (in milliseconds) from a scroll speed in the range of 1-40.
/// </summary>
/// <param name="scrollSpeed">The scroll speed.</param>
/// <returns>The scroll time.</returns>
public static double ComputeScrollTime(int scrollSpeed) => MAX_TIME_RANGE / scrollSpeed;
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();

View File

@ -17,6 +17,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutoplay : OsuModTestScene
{
[Test]
public void TestCursorPositionStoredToJudgement()
{
CreateModTest(new ModTestData
{
Autoplay = true,
PassCondition = () =>
Player.ScoreProcessor.JudgedHits >= 1
&& Player.ScoreProcessor.HitEvents.Any(e => e.Position != null)
});
}
[Test]
public void TestSpmUnaffectedByRateAdjust()
=> runSpmTest(new OsuModDaycore

View File

@ -42,14 +42,14 @@ namespace osu.Game.Rulesets.Osu.Mods
private PlayfieldAdjustmentContainer bubbleContainer = null!;
private DrawablePool<BubbleDrawable> bubblePool = null!;
private readonly Bindable<int> currentCombo = new BindableInt();
private float maxSize;
private float bubbleSize;
private double bubbleFade;
private readonly DrawablePool<BubbleDrawable> bubblePool = new DrawablePool<BubbleDrawable>(100);
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
@ -72,6 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
bubbleContainer = drawableRuleset.CreatePlayfieldAdjustmentContainer();
drawableRuleset.Overlays.Add(bubbleContainer);
drawableRuleset.Overlays.Add(bubblePool = new DrawablePool<BubbleDrawable>(100));
}
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)

View File

@ -291,56 +291,32 @@ namespace osu.Game.Rulesets.Osu
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();
return new[]
{
new StatisticRow
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{
Columns = new[]
{
new StatisticItem("Accuracy Heatmap", () => new AccuracyHeatmap(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
}
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
};
}

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
@ -13,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
}
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 700000 * comboProgress

View File

@ -229,45 +229,27 @@ namespace osu.Game.Rulesets.Taiko
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();
return new[]
{
new StatisticRow
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
Columns = new[]
{
new StatisticItem("Performance Breakdown", () => new PerformanceBreakdownChart(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}),
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
Columns = new[]
{
new StatisticItem("Timing Distribution", () => new HitEventTimingDistributionGraph(timedHitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
}
},
new StatisticRow
RelativeSizeAxes = Axes.X,
Height = 250
}, true),
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
Columns = new[]
{
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
{
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
}
}
new AverageHitError(timedHitEvents),
new UnstableRate(timedHitEvents)
}), true)
};
}
}

View File

@ -264,8 +264,9 @@ namespace osu.Game.Tests.Visual.Collections
assertCollectionName(1, "First");
}
[Test]
public void TestCollectionRenamedOnTextChange()
[TestCase(false)]
[TestCase(true)]
public void TestCollectionRenamedOnTextChange(bool commitWithEnter)
{
BeatmapCollection first = null!;
DrawableCollectionListItem firstItem = null!;
@ -293,9 +294,19 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("change first collection name", () =>
{
firstItem.ChildrenOfType<TextBox>().First().Text = "First";
InputManager.Key(Key.Enter);
});
if (commitWithEnter)
AddStep("commit via enter", () => InputManager.Key(Key.Enter));
else
{
AddStep("commit via click away", () =>
{
InputManager.MoveMouseTo(firstItem.ScreenSpaceDrawQuad.TopLeft - new Vector2(10));
InputManager.Click(MouseButton.Left);
});
}
AddUntilStep("collection has new name", () => first.Name == "First");
}

View File

@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Playlist =
{
new MultiplayerPlaylistItem(playlistItem),
TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
},
Users = { localUser },
Host = localUser,

View File

@ -906,7 +906,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@ -938,7 +938,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,

View File

@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now

View File

@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add playlist item", () =>
{
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();

View File

@ -17,6 +17,7 @@ using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
@ -539,6 +540,11 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("open room", () => multiplayerComponents.ChildrenOfType<LoungeSubScreen>().Single().Open());
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddWaitStep("wait two frames", 2);
AddStep("exit lounge", () => Game.ScreenStack.Exit());
// `TestMultiplayerComponents` registers a request handler in its BDL, but never unregisters it.
// to prevent the handler living for longer than it should be, clean up manually.
AddStep("clean up multiplayer request handler", () => ((DummyAPIAccess)API).HandleRequest = null);
}
[Test]

View File

@ -174,78 +174,33 @@ namespace osu.Game.Tests.Visual.Ranking
private class TestRulesetAllStatsRequireHitEvents : TestRuleset
{
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
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)
}
}
};
}
new StatisticItem("Statistic Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
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)
public override StatisticItem[] 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"))
}
}
new StatisticItem("Statistic Not Requiring Hit Events 1", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events")),
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)
public override StatisticItem[] 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"))
}
}
new StatisticItem("Statistic Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Requires hit events"), true),
new StatisticItem("Statistic Not Requiring Hit Events", () => CreatePlaceholderStatistic("Placeholder statistic. Does not require hit events"))
};
}
}

View File

@ -86,6 +86,7 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
CommitOnFocusLost = true,
PlaceholderText = collection.IsManaged ? string.Empty : "Create a new collection"
},
}

View File

@ -179,43 +179,9 @@ namespace osu.Game.Database
applyFilenameSchemaSuffix(ref Filename);
#endif
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available.
if (storage.Exists(newerVersionFilename))
{
Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
attemptRecoverFromFile(newerVersionFilename);
}
try
{
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
cleanupPendingDeletions();
}
catch (Exception e)
{
// See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
// This is the best way we can detect a schema version downgrade.
if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
{
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename))
createBackup(newerVersionFilename);
storage.Delete(Filename);
}
else
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
storage.Delete(Filename);
}
cleanupPendingDeletions();
}
// `prepareFirstRealmAccess()` triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
using (var realm = prepareFirstRealmAccess())
cleanupPendingDeletions(realm);
}
/// <summary>
@ -312,49 +278,93 @@ namespace osu.Game.Database
Logger.Log(@"Recovery complete!", LoggingTarget.Database);
}
private void cleanupPendingDeletions()
private Realm prepareFirstRealmAccess()
{
using (var realm = getRealmInstance())
using (var transaction = realm.BeginWrite())
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available.
if (storage.Exists(newerVersionFilename))
{
var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
foreach (var score in pendingDeleteScores)
realm.Remove(score);
var pendingDeleteSets = realm.All<BeatmapSetInfo>().Where(s => s.DeletePending);
foreach (var beatmapSet in pendingDeleteSets)
{
foreach (var beatmap in beatmapSet.Beatmaps)
{
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
foreach (var score in beatmap.Scores)
realm.Remove(score);
realm.Remove(beatmap.Metadata);
realm.Remove(beatmap);
}
realm.Remove(beatmapSet);
}
var pendingDeleteSkins = realm.All<SkinInfo>().Where(s => s.DeletePending);
foreach (var s in pendingDeleteSkins)
realm.Remove(s);
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
foreach (var s in pendingDeletePresets)
realm.Remove(s);
transaction.Commit();
Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
attemptRecoverFromFile(newerVersionFilename);
}
// clean up files after dropping any pending deletions.
// in the future we may want to only do this when the game is idle, rather than on every startup.
new RealmFileStore(this, storage).Cleanup();
try
{
return getRealmInstance();
}
catch (Exception e)
{
// See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
// This is the best way we can detect a schema version downgrade.
if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
{
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename))
createBackup(newerVersionFilename);
}
else
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
}
storage.Delete(Filename);
return getRealmInstance();
}
}
private void cleanupPendingDeletions(Realm realm)
{
try
{
using (var transaction = realm.BeginWrite())
{
var pendingDeleteScores = realm.All<ScoreInfo>().Where(s => s.DeletePending);
foreach (var score in pendingDeleteScores)
realm.Remove(score);
var pendingDeleteSets = realm.All<BeatmapSetInfo>().Where(s => s.DeletePending);
foreach (var beatmapSet in pendingDeleteSets)
{
foreach (var beatmap in beatmapSet.Beatmaps)
{
// Cascade delete related scores, else they will have a null beatmap against the model's spec.
foreach (var score in beatmap.Scores)
realm.Remove(score);
realm.Remove(beatmap.Metadata);
realm.Remove(beatmap);
}
realm.Remove(beatmapSet);
}
var pendingDeleteSkins = realm.All<SkinInfo>().Where(s => s.DeletePending);
foreach (var s in pendingDeleteSkins)
realm.Remove(s);
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
foreach (var s in pendingDeletePresets)
realm.Remove(s);
transaction.Commit();
}
// clean up files after dropping any pending deletions.
// in the future we may want to only do this when the game is idle, rather than on every startup.
new RealmFileStore(this, storage).Cleanup();
}
catch (Exception e)
{
Logger.Error(e, "Failed to clean up unused files. This is not critical but please report if it happens regularly.");
}
}
/// <summary>
@ -909,7 +919,7 @@ namespace osu.Game.Database
int attempts = 10;
while (attempts-- > 0)
while (true)
{
try
{
@ -927,6 +937,9 @@ namespace osu.Game.Database
}
catch (IOException)
{
if (attempts-- <= 0)
throw;
// file may be locked during use.
Thread.Sleep(500);
}

View File

@ -19,6 +19,7 @@ using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Input.Bindings;
using osu.Game.Online.Multiplayer;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
@ -69,7 +70,7 @@ namespace osu.Game.Graphics
{
case GlobalAction.TakeScreenshot:
shutter.Play();
TakeScreenshotAsync();
TakeScreenshotAsync().FireAndForget();
return true;
}
@ -86,70 +87,75 @@ namespace osu.Game.Graphics
{
Interlocked.Increment(ref screenShotTasks);
if (!captureMenuCursor.Value)
try
{
cursorVisibility.Value = false;
// We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
const int frames_to_wait = 3;
int framesWaited = 0;
using (var framesWaitedEvent = new ManualResetEventSlim(false))
if (!captureMenuCursor.Value)
{
ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
cursorVisibility.Value = false;
// We need to wait for at most 3 draw nodes to be drawn, following which we can be assured at least one DrawNode has been generated/drawn with the set value
const int frames_to_wait = 3;
int framesWaited = 0;
using (var framesWaitedEvent = new ManualResetEventSlim(false))
{
if (framesWaited++ >= frames_to_wait)
// ReSharper disable once AccessToDisposedClosure
framesWaitedEvent.Set();
}, 10, true);
ScheduledDelegate waitDelegate = host.DrawThread.Scheduler.AddDelayed(() =>
{
if (framesWaited++ >= frames_to_wait)
// ReSharper disable once AccessToDisposedClosure
framesWaitedEvent.Set();
}, 10, true);
if (!framesWaitedEvent.Wait(1000))
throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
if (!framesWaitedEvent.Wait(1000))
throw new TimeoutException("Screenshot data did not arrive in a timely fashion");
waitDelegate.Cancel();
waitDelegate.Cancel();
}
}
using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
{
host.GetClipboard()?.SetImage(image);
(string filename, var stream) = getWritableStream();
if (filename == null) return;
using (stream)
{
switch (screenshotFormat.Value)
{
case ScreenshotFormat.Png:
await image.SaveAsPngAsync(stream).ConfigureAwait(false);
break;
case ScreenshotFormat.Jpg:
const int jpeg_quality = 92;
await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
break;
default:
throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
}
}
notificationOverlay.Post(new SimpleNotification
{
Text = $"Screenshot {filename} saved!",
Activated = () =>
{
storage.PresentFileExternally(filename);
return true;
}
});
}
}
using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false))
finally
{
if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false)
if (Interlocked.Decrement(ref screenShotTasks) == 0)
cursorVisibility.Value = true;
host.GetClipboard()?.SetImage(image);
(string filename, var stream) = getWritableStream();
if (filename == null) return;
using (stream)
{
switch (screenshotFormat.Value)
{
case ScreenshotFormat.Png:
await image.SaveAsPngAsync(stream).ConfigureAwait(false);
break;
case ScreenshotFormat.Jpg:
const int jpeg_quality = 92;
await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false);
break;
default:
throw new InvalidOperationException($"Unknown enum member {nameof(ScreenshotFormat)} {screenshotFormat.Value}.");
}
}
notificationOverlay.Post(new SimpleNotification
{
Text = $"Screenshot {filename} saved!",
Activated = () =>
{
storage.PresentFileExternally(filename);
return true;
}
});
}
});

View File

@ -152,7 +152,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour osuColour)
{
background.Colour = colourProvider?.Background5 ?? Color4Extensions.FromHex(@"1c2125");
background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125");
descriptionText.Colour = osuColour.Yellow;
}

View File

@ -22,6 +22,9 @@ namespace osu.Game.Localisation
[Description(@"Български")]
bg,
[Description(@"Català")]
ca,
[Description(@"Česky")]
cs,
@ -37,12 +40,27 @@ namespace osu.Game.Localisation
[Description(@"español")]
es,
// TODO: Requires Arabic glyphs to be added to resources (and possibly also RTL support).
// [Description(@"فارسی")]
// fa_ir,
[Description(@"Suomi")]
fi,
// TODO: Doesn't work as appropriate satellite assemblies aren't copied from resources (see: https://github.com/ppy/osu/discussions/18851#discussioncomment-3042170)
// [Description(@"Filipino")]
// fil,
[Description(@"français")]
fr,
// TODO: Requires Hebrew glyphs to be added to resources (and possibly also RTL support).
// [Description(@"עברית")]
// he,
[Description(@"Hrvatski")]
hr_hr,
[Description(@"Magyar")]
hu,
@ -58,6 +76,15 @@ namespace osu.Game.Localisation
[Description(@"한국어")]
ko,
[Description(@"Lietuvių")]
lt,
[Description(@"Latviešu")]
lv_lv,
[Description(@"Melayu")]
ms_my,
[Description(@"Nederlands")]
nl,
@ -79,12 +106,28 @@ namespace osu.Game.Localisation
[Description(@"Русский")]
ru,
// TODO: Requires Sinhala glyphs to be added to resources.
// Additionally, no translations available yet.
// [Description(@"සිංහල")]
// si_lk,
[Description(@"Slovenčina")]
sk,
[Description(@"Slovenščina")]
sl,
[Description(@"Српски")]
sr,
[Description(@"Svenska")]
sv,
// Tajik has no associated localisations yet, and is not supported on Windows versions <10.
// TODO: update language mapping in osu-resources to redirect tg-TJ to tg-Cyrl-TJ (which is supported on earlier Windows versions)
// [Description(@"Тоҷикӣ")]
// tg_tj,
[Description(@"ไทย")]
th,

View File

@ -82,7 +82,7 @@ namespace osu.Game.Localisation
/// <summary>
/// "{0}ms (speed {1})"
/// </summary>
public static LocalisableString ScrollSpeedTooltip(double arg0, int arg1) => new TranslatableString(getKey(@"ruleset"), @"{0}ms (speed {1})", arg0, arg1);
public static LocalisableString ScrollSpeedTooltip(double scrollTime, int scrollSpeed) => new TranslatableString(getKey(@"ruleset"), @"{0:0}ms (speed {1})", scrollTime, scrollSpeed);
private static string getKey(string key) => $@"{prefix}:{key}";
}

View File

@ -783,7 +783,7 @@ namespace osu.Game.Online.Multiplayer
RoomUpdated?.Invoke();
}
private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID })
private PlaylistItem createPlaylistItem(MultiplayerPlaylistItem item) => new PlaylistItem(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
{
ID = item.ID,
OwnerID = item.OwnerID,

View File

@ -53,22 +53,12 @@ namespace osu.Game.Online.Rooms
[Key(9)]
public DateTimeOffset? PlayedAt { get; set; }
[Key(10)]
public double StarRating { get; set; }
[SerializationConstructor]
public MultiplayerPlaylistItem()
{
}
public MultiplayerPlaylistItem(PlaylistItem item)
{
ID = item.ID;
OwnerID = item.OwnerID;
BeatmapID = item.Beatmap.OnlineID;
BeatmapChecksum = item.Beatmap.MD5Hash;
RulesetID = item.RulesetID;
RequiredMods = item.RequiredMods.ToArray();
AllowedMods = item.AllowedMods.ToArray();
Expired = item.Expired;
PlaylistOrder = item.PlaylistOrder ?? 0;
PlayedAt = item.PlayedAt;
}
}
}

View File

@ -91,7 +91,7 @@ namespace osu.Game.Online.Rooms
}
public PlaylistItem(MultiplayerPlaylistItem item)
: this(new APIBeatmap { OnlineID = item.BeatmapID })
: this(new APIBeatmap { OnlineID = item.BeatmapID, StarRating = item.StarRating })
{
ID = item.ID;
OwnerID = item.OwnerID;

View File

@ -1136,12 +1136,22 @@ namespace osu.Game
if (entry.Level == LogLevel.Error)
{
Schedule(() => Notifications.Post(new SimpleNotification
Schedule(() =>
{
Text = $"Encountered tablet error: \"{message}\"",
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.RedDark,
}));
Notifications.Post(new SimpleNotification
{
Text = $"Disabling tablet support due to error: \"{message}\"",
Icon = FontAwesome.Solid.PenSquare,
IconColour = Colours.RedDark,
});
// We only have one tablet handler currently.
// The loop here is weakly guarding against a future where more than one is added.
// If this is ever the case, this logic needs adjustment as it should probably only
// disable the relevant tablet handler rather than all.
foreach (var tabletHandler in Host.AvailableInputHandlers.OfType<ITabletHandler>())
tabletHandler.Enabled.Value = false;
});
}
else if (notifyOnWarning)
{

View File

@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Difficulty
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
Mod classicMod = rulesetInstance.CreateMod<ModClassic>();
var finalCombination = ModUtils.FlattenMod(combination);
if (classicMod != null)

View File

@ -321,8 +321,8 @@ namespace osu.Game.Rulesets
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to create the statistics for. The score is guaranteed to have <see cref="ScoreInfo.HitEvents"/> populated.</param>
/// <param name="playableBeatmap">The <see cref="IBeatmap"/>, converted for this <see cref="Ruleset"/> with all relevant <see cref="Mod"/>s applied.</param>
/// <returns>The <see cref="StatisticRow"/>s to display. Each <see cref="StatisticRow"/> may contain 0 or more <see cref="StatisticItem"/>.</returns>
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticRow>();
/// <returns>The <see cref="StatisticItem"/>s to display.</returns>
public virtual StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticItem>();
/// <summary>
/// Get all valid <see cref="HitResult"/>s for this ruleset.

View File

@ -50,6 +50,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Container colouredComponents;
private readonly OsuSpriteText comboIndexText;
private readonly SamplePointPiece samplePointPiece;
private readonly DifficultyPointPiece? difficultyPointPiece;
[Resolved]
private ISkinSource skin { get; set; } = null!;
@ -101,7 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
},
}
},
new SamplePointPiece(Item)
samplePointPiece = new SamplePointPiece(Item)
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopCentre
@ -118,7 +120,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (item is IHasSliderVelocity)
{
AddInternal(new DifficultyPointPiece(Item)
AddInternal(difficultyPointPiece = new DifficultyPointPiece(Item)
{
Anchor = Anchor.TopLeft,
Origin = Anchor.BottomCentre
@ -244,6 +246,25 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public override Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.TopLeft;
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds)
{
// Since children are exceeding the component size, we need to use a custom quad to compute whether it should be masked away.
// If the component isn't considered masked away by itself, there's no need to apply custom logic.
if (!base.ComputeIsMaskedAway(maskingBounds))
return false;
// If the component is considered masked away, we'll use children to create an extended quad that encapsulates all parts of this blueprint
// to ensure it doesn't pop in and out of existence abruptly when scrolling the timeline.
var rect = RectangleF.Union(ScreenSpaceDrawQuad.AABBFloat, circle.ScreenSpaceDrawQuad.AABBFloat);
rect = RectangleF.Union(rect, samplePointPiece.ScreenSpaceDrawQuad.AABBFloat);
if (difficultyPointPiece != null)
rect = RectangleF.Union(rect, difficultyPointPiece.ScreenSpaceDrawQuad.AABBFloat);
return !Precision.AlmostIntersects(maskingBounds, rect);
}
private partial class Tick : Circle
{
public Tick()

View File

@ -85,15 +85,15 @@ namespace osu.Game.Screens.OnlinePlay.Components
StarDifficulty minDifficulty;
StarDifficulty maxDifficulty;
if (DifficultyRange.Value != null)
if (DifficultyRange.Value != null && Playlist.Count == 0)
{
// When Playlist is empty (in lounge) we take retrieved range
minDifficulty = new StarDifficulty(DifficultyRange.Value.Min, 0);
maxDifficulty = new StarDifficulty(DifficultyRange.Value.Max, 0);
}
else
{
// In multiplayer rooms, the beatmaps of playlist items will not be populated to a point this can be correct.
// Either populating them via BeatmapLookupCache or polling the API for the room's DifficultyRange will be required.
// When Playlist is not empty (in room) we compute actual range
var orderedDifficulties = Playlist.Select(p => p.Beatmap).OrderBy(b => b.StarRating).ToArray();
minDifficulty = new StarDifficulty(orderedDifficulties.Length > 0 ? orderedDifficulties[0].StarRating : 0, 0);

View File

@ -17,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// Represents a table with simple statistics (ones that only need textual display).
/// Richer visualisations should be done with <see cref="StatisticRow"/>s and <see cref="StatisticItem"/>s.
/// Richer visualisations should be done with <see cref="StatisticItem"/>s.
/// </summary>
public partial class SimpleStatisticTable : CompositeDrawable
{

View File

@ -23,32 +23,26 @@ namespace osu.Game.Screens.Ranking.Statistics
public Bindable<SoloStatisticsUpdate?> StatisticsUpdate { get; } = new Bindable<SoloStatisticsUpdate?>();
protected override ICollection<StatisticRow> CreateStatisticRows(ScoreInfo newScore, IBeatmap playableBeatmap)
protected override ICollection<StatisticItem> CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap)
{
var rows = base.CreateStatisticRows(newScore, playableBeatmap);
var items = base.CreateStatisticItems(newScore, playableBeatmap);
if (newScore.UserID > 1
&& newScore.UserID == achievedScore.UserID
&& newScore.OnlineID > 0
&& newScore.OnlineID == achievedScore.OnlineID)
{
rows = rows.Append(new StatisticRow
items = items.Append(new StatisticItem("Overall Ranking", () => new OverallRanking
{
Columns = new[]
{
new StatisticItem("Overall Ranking", () => new OverallRanking
{
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
StatisticsUpdate = { BindTarget = StatisticsUpdate }
})
}
}).ToArray();
RelativeSizeAxes = Axes.X,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
StatisticsUpdate = { BindTarget = StatisticsUpdate }
})).ToArray();
}
return rows;
return items;
}
}
}

View File

@ -6,7 +6,6 @@
using System;
using JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
namespace osu.Game.Screens.Ranking.Statistics
@ -26,29 +25,22 @@ namespace osu.Game.Screens.Ranking.Statistics
/// </summary>
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;
/// <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 in the results screen.
/// </summary>
/// <param name="name">The name of the item. Can be <see langword="null"/> to hide the item header.</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(LocalisableString name, [NotNull] Func<Drawable> createContent, bool requiresHitEvents = false, [CanBeNull] Dimension dimension = null)
public StatisticItem(LocalisableString name, [NotNull] Func<Drawable> createContent, bool requiresHitEvents = false)
{
Name = name;
RequiresHitEvents = requiresHitEvents;
CreateContent = createContent;
Dimension = dimension;
}
}
}

View File

@ -1,21 +0,0 @@
// 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.
#nullable disable
using JetBrains.Annotations;
namespace osu.Game.Screens.Ranking.Statistics
{
/// <summary>
/// A row of statistics to be displayed in the results screen.
/// </summary>
public class StatisticRow
{
/// <summary>
/// The columns of this <see cref="StatisticRow"/>.
/// </summary>
[ItemNotNull]
public StatisticItem[] Columns;
}
}

View File

@ -100,9 +100,9 @@ namespace osu.Game.Screens.Ranking.Statistics
bool hitEventsAvailable = newScore.HitEvents.Count != 0;
Container<Drawable> container;
var statisticRows = CreateStatisticRows(newScore, task.GetResultSafely());
var statisticItems = CreateStatisticItems(newScore, task.GetResultSafely());
if (!hitEventsAvailable && statisticRows.SelectMany(r => r.Columns).All(c => c.RequiresHitEvents))
if (!hitEventsAvailable && statisticItems.All(c => c.RequiresHitEvents))
{
container = new FillFlowContainer
{
@ -144,33 +144,22 @@ namespace osu.Game.Screens.Ranking.Statistics
bool anyRequiredHitEvents = false;
foreach (var row in statisticRows)
foreach (var item in statisticItems)
{
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 && item.RequiresHitEvents)
{
if (!hitEventsAvailable && col.RequiresHitEvents)
{
anyRequiredHitEvents = true;
continue;
}
columnContent.Add(new StatisticContainer(col)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
dimensions.Add(col.Dimension ?? new Dimension());
anyRequiredHitEvents = true;
continue;
}
columnContent.Add(new StatisticContainer(item)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
rows.Add(new GridContainer
{
Anchor = Anchor.TopCentre,
@ -178,7 +167,7 @@ namespace osu.Game.Screens.Ranking.Statistics
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { columnContent.ToArray() },
ColumnDimensions = dimensions.ToArray(),
ColumnDimensions = new[] { new Dimension() },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
});
}
@ -219,11 +208,11 @@ namespace osu.Game.Screens.Ranking.Statistics
}
/// <summary>
/// Creates the <see cref="StatisticRow"/>s to be displayed in this panel for a given <paramref name="newScore"/>.
/// Creates the <see cref="StatisticItem"/>s to be displayed in this panel for a given <paramref name="newScore"/>.
/// </summary>
/// <param name="newScore">The score to create the rows for.</param>
/// <param name="playableBeatmap">The beatmap on which the score was set.</param>
protected virtual ICollection<StatisticRow> CreateStatisticRows(ScoreInfo newScore, IBeatmap playableBeatmap)
protected virtual ICollection<StatisticItem> CreateStatisticItems(ScoreInfo newScore, IBeatmap playableBeatmap)
=> newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap);
protected override bool OnClick(ClickEvent e)

View File

@ -108,7 +108,8 @@ namespace osu.Game.Screens.Select.Carousel
Header.Children = new Drawable[]
{
background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault()))
// Choice of background image matches BSS implementation (always uses the lowest `beatmap_id` from the set).
background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)))
{
RelativeSizeAxes = Axes.Both,
}, 300)

View File

@ -108,7 +108,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
// simulate the server's automatic assignment of users to teams on join.
// the "best" team is the one with the least users on it.
int bestTeam = teamVersus.Teams
.Select(team => (teamID: team.ID, userCount: ServerRoom.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID))).MinBy(pair => pair.userCount).teamID;
.Select(team => (teamID: team.ID, userCount: ServerRoom.Users.Count(u => (u.MatchState as TeamVersusUserState)?.TeamID == team.ID)))
.MinBy(pair => pair.userCount).teamID;
user.MatchState = new TeamVersusUserState { TeamID = bestTeam };
((IMultiplayerClient)this).MatchUserStateChanged(clone(user.UserID), clone(user.MatchState)).WaitSafely();
@ -232,7 +233,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
QueueMode = ServerAPIRoom.QueueMode.Value,
AutoStartDuration = ServerAPIRoom.AutoStartDuration.Value
},
Playlist = ServerAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)).ToList(),
Playlist = ServerAPIRoom.Playlist.Select(CreateMultiplayerPlaylistItem).ToList(),
Users = { localUser },
Host = localUser
};
@ -637,5 +638,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
byte[]? serialized = MessagePackSerializer.Serialize(typeof(T), incoming, SignalRUnionWorkaroundResolver.OPTIONS);
return MessagePackSerializer.Deserialize<T>(serialized, SignalRUnionWorkaroundResolver.OPTIONS);
}
public static MultiplayerPlaylistItem CreateMultiplayerPlaylistItem(PlaylistItem item) => new MultiplayerPlaylistItem
{
ID = item.ID,
OwnerID = item.OwnerID,
BeatmapID = item.Beatmap.OnlineID,
BeatmapChecksum = item.Beatmap.MD5Hash,
RulesetID = item.RulesetID,
RequiredMods = item.RequiredMods.ToArray(),
AllowedMods = item.AllowedMods.ToArray(),
Expired = item.Expired,
PlaylistOrder = item.PlaylistOrder ?? 0,
PlayedAt = item.PlayedAt,
StarRating = item.Beatmap.StarRating,
};
}
}

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.521.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.531.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.510.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.521.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.531.0" />
</ItemGroup>
</Project>