mirror of
https://github.com/ppy/osu
synced 2025-03-11 05:49:12 +00:00
Merge branch 'master' into add-last-edit-time
This commit is contained in:
commit
3a50e0fb2f
@ -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" />
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -129,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
Playlist =
|
||||
{
|
||||
new MultiplayerPlaylistItem(playlistItem),
|
||||
TestMultiplayerClient.CreateMultiplayerPlaylistItem(playlistItem),
|
||||
},
|
||||
Users = { localUser },
|
||||
Host = localUser,
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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"))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
},
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
||||
|
@ -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}";
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user