1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-16 00:02:54 +08:00

Merge branch 'master' into editor-exit-harsh-blocking

This commit is contained in:
Dan Balasescu 2022-03-23 07:06:25 +09:00 committed by GitHub
commit 8591630e5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 367 additions and 89 deletions

5
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"recommendations": [
"ms-dotnettools.csharp"
]
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModClassic : ModClassic, IApplicableToHitObject, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override Type[] IncompatibleMods => new[] { typeof(OsuModStrictTracking) };
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true); public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);

View File

@ -0,0 +1,148 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModStrictTracking : Mod, IApplicableAfterBeatmapConversion, IApplicableToDrawableHitObject, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => @"Strict Tracking";
public override string Acronym => @"ST";
public override IconUsage? Icon => FontAwesome.Solid.PenFancy;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => @"Follow circles just got serious...";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModClassic) };
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider slider)
{
slider.Tracking.ValueChanged += e =>
{
if (e.NewValue || slider.Judged) return;
var tail = slider.NestedHitObjects.OfType<StrictTrackingDrawableSliderTail>().First();
if (!tail.Judged)
tail.MissForcefully();
};
}
}
public void ApplyToBeatmap(IBeatmap beatmap)
{
var osuBeatmap = (OsuBeatmap)beatmap;
if (osuBeatmap.HitObjects.Count == 0) return;
var hitObjects = osuBeatmap.HitObjects.Select(ho =>
{
if (ho is Slider slider)
{
var newSlider = new StrictTrackingSlider(slider);
return newSlider;
}
return ho;
}).ToList();
osuBeatmap.HitObjects = hitObjects;
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
drawableRuleset.Playfield.RegisterPool<StrictTrackingSliderTailCircle, StrictTrackingDrawableSliderTail>(10, 100);
}
private class StrictTrackingSliderTailCircle : SliderTailCircle
{
public StrictTrackingSliderTailCircle(Slider slider)
: base(slider)
{
}
public override Judgement CreateJudgement() => new OsuJudgement();
}
private class StrictTrackingDrawableSliderTail : DrawableSliderTail
{
public override bool DisplayResult => true;
}
private class StrictTrackingSlider : Slider
{
public StrictTrackingSlider(Slider original)
{
StartTime = original.StartTime;
Samples = original.Samples;
Path = original.Path;
NodeSamples = original.NodeSamples;
RepeatCount = original.RepeatCount;
Position = original.Position;
NewCombo = original.NewCombo;
ComboOffset = original.ComboOffset;
LegacyLastTickOffset = original.LegacyLastTickOffset;
TickDistanceMultiplier = original.TickDistanceMultiplier;
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
var sliderEvents = SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken);
foreach (var e in sliderEvents)
{
switch (e.Type)
{
case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle
{
StartTime = e.Time,
Position = Position,
StackHeight = StackHeight,
});
break;
case SliderEventType.LegacyLastTick:
AddNested(TailCircle = new StrictTrackingSliderTailCircle(this)
{
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
});
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
}
}
UpdateNestedSamples();
}
}
}
}

View File

@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider() public Slider()
{ {
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples(); SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions(); Path.Version.ValueChanged += _ => updateNestedPositions();
} }
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
} }
updateNestedSamples(); UpdateNestedSamples();
} }
private void updateNestedPositions() private void updateNestedPositions()
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
TailCircle.Position = EndPosition; TailCircle.Position = EndPosition;
} }
private void updateNestedSamples() protected void UpdateNestedSamples()
{ {
var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) var firstSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)
?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933) ?? Samples.FirstOrDefault(); // TODO: remove this when guaranteed sort is present for samples (https://github.com/ppy/osu/issues/1933)

View File

@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()), new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
new OsuModHidden(), new OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()), new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
switch (osuComponent.Component) switch (osuComponent.Component)
{ {
case OsuSkinComponents.FollowPoint: case OsuSkinComponents.FollowPoint:
return this.GetAnimation(component.LookupName, true, false, true, startAtCurrentTime: false); return this.GetAnimation(component.LookupName, true, true, true, startAtCurrentTime: false);
case OsuSkinComponents.SliderFollowCircle: case OsuSkinComponents.SliderFollowCircle:
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true); var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);

View File

@ -590,6 +590,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -597,6 +599,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending); Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending); Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
}); });
} }
@ -646,6 +649,8 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.DeletePending); Assert.IsTrue(imported.DeletePending);
var originalAddedDate = imported.DateAdded;
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm); var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
@ -653,6 +658,7 @@ namespace osu.Game.Tests.Database
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
Assert.IsFalse(imported.DeletePending); Assert.IsFalse(imported.DeletePending);
Assert.IsFalse(importedSecondTime.DeletePending); Assert.IsFalse(importedSecondTime.DeletePending);
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
}); });
} }

View File

@ -133,7 +133,6 @@ namespace osu.Game.Tests.Resources
StarRating = diff, StarRating = diff,
Length = length, Length = length,
BPM = bpm, BPM = bpm,
MaxCombo = 1000,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo, Ruleset = rulesetInfo,
Metadata = metadata, Metadata = metadata,

View File

@ -1,14 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning.Editor; using osu.Game.Skinning.Editor;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay namespace osu.Game.Tests.Visual.Gameplay
{ {
@ -29,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reload skin editor", () => AddStep("reload skin editor", () =>
{ {
skinEditor?.Expire(); skinEditor?.Expire();
Player.ScaleTo(0.8f); Player.ScaleTo(0.4f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add); LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
}); });
} }
@ -40,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility()); AddToggleStep("toggle editor visibility", visible => skinEditor.ToggleVisibility());
} }
[Test]
public void TestEditComponent()
{
BarHitErrorMeter hitErrorMeter = null;
AddStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First(b => b.Item is BarHitErrorMeter);
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
} }
} }

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
@ -21,10 +23,12 @@ using osu.Game.Scoring;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Screens.Select.Options; using osu.Game.Screens.Select.Options;
using osu.Game.Skinning.Editor;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
} }
[Test]
public void TestEditComponentDuringGameplay()
{
Screens.Select.SongSelect songSelect = null;
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
SkinEditor skinEditor = null;
AddStep("open skin editor", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.ShiftLeft);
InputManager.Key(Key.S);
InputManager.ReleaseKey(Key.ControlLeft);
InputManager.ReleaseKey(Key.ShiftLeft);
});
AddUntilStep("get skin editor", () => (skinEditor = Game.ChildrenOfType<SkinEditor>().FirstOrDefault()) != null);
AddStep("Click gameplay scene button", () =>
{
skinEditor.ChildrenOfType<SkinEditorSceneLibrary.SceneButton>().First(b => b.Text == "Gameplay").TriggerClick();
});
AddUntilStep("wait for player", () =>
{
// dismiss any notifications that may appear (ie. muted notification).
clickMouseInCentre();
return Game.ScreenStack.CurrentScreen is Player;
});
BarHitErrorMeter hitErrorMeter = null;
AddUntilStep("select bar hit error blueprint", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().FirstOrDefault(b => b.Item is BarHitErrorMeter);
if (blueprint == null)
return false;
hitErrorMeter = (BarHitErrorMeter)blueprint.Item;
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
return true;
});
AddAssert("value is default", () => hitErrorMeter.JudgementLineThickness.IsDefault);
AddStep("hover first slider", () =>
{
InputManager.MoveMouseTo(
skinEditor.ChildrenOfType<SkinSettingsToolbox>().First()
.ChildrenOfType<SettingsSlider<float>>().First()
.ChildrenOfType<SliderBar<float>>().First()
);
});
AddStep("adjust slider via keyboard", () => InputManager.Key(Key.Left));
AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default);
}
[Test] [Test]
public void TestRetryCountIncrements() public void TestRetryCountIncrements()
{ {
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local)); AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);
@ -152,7 +224,8 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action()); AddStep("press back button", () => Game.ChildrenOfType<BackButton>().First().Action());
AddStep("show local scores", () => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local)); AddStep("show local scores",
() => Game.ChildrenOfType<BeatmapDetailAreaTabControl>().First().Current.Value = new BeatmapDetailAreaLeaderboardTabItem<BeatmapLeaderboardScope>(BeatmapLeaderboardScope.Local));
AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null); AddUntilStep("wait for score displayed", () => (scorePanel = Game.ChildrenOfType<LeaderboardScore>().FirstOrDefault(s => s.Score.Equals(score))) != null);

View File

@ -9,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using Realms; using Realms;
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
[Ignored] [Ignored]
public APIBeatmap? OnlineInfo { get; set; } public APIBeatmap? OnlineInfo { get; set; }
/// <summary>
/// The maximum achievable combo on this beatmap, populated for online info purposes only.
/// Todo: This should never be used nor exist, but is still relied on in <see cref="ScoresContainer.Scores"/> since <see cref="IBeatmapInfo"/> can't be used yet. For now this is obsoleted until it is removed.
/// </summary>
[Ignored] [Ignored]
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
public int? MaxCombo { get; set; } public int? MaxCombo { get; set; }
[Ignored] [Ignored]

View File

@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public APIBeatmap OnlineInfo { get; set; } public APIBeatmap OnlineInfo { get; set; }
[NotMapped]
public int? MaxCombo { get; set; }
/// <summary> /// <summary>
/// The playable length in milliseconds of this beatmap. /// The playable length in milliseconds of this beatmap.
/// </summary> /// </summary>

View File

@ -295,7 +295,6 @@ namespace osu.Game.Database
TimelineZoom = beatmap.TimelineZoom, TimelineZoom = beatmap.TimelineZoom,
Countdown = beatmap.Countdown, Countdown = beatmap.Countdown,
CountdownOffset = beatmap.CountdownOffset, CountdownOffset = beatmap.CountdownOffset,
MaxCombo = beatmap.MaxCombo,
Bookmarks = beatmap.Bookmarks, Bookmarks = beatmap.Bookmarks,
BeatmapSet = realmBeatmapSet, BeatmapSet = realmBeatmapSet,
}; };

View File

@ -1046,6 +1046,10 @@ namespace osu.Game
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.ToggleSkinEditor:
skinEditor.ToggleVisibility();
return true;
case GlobalAction.ResetInputSettings: case GlobalAction.ResetInputSettings:
Host.ResetInputHandlers(); Host.ResetInputHandlers();
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault(); frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();

View File

@ -173,7 +173,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{ {
Text = score.MaxCombo.ToLocalisableString(@"0\x"), Text = score.MaxCombo.ToLocalisableString(@"0\x"),
Font = OsuFont.GetFont(size: text_size), Font = OsuFont.GetFont(size: text_size),
#pragma warning disable 618
Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
#pragma warning restore 618
} }
}; };

View File

@ -78,7 +78,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
// TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`. // TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`.
var beatmapInfo = new BeatmapInfo var beatmapInfo = new BeatmapInfo
{ {
#pragma warning disable 618
MaxCombo = apiBeatmap.MaxCombo, MaxCombo = apiBeatmap.MaxCombo,
#pragma warning restore 618
Status = apiBeatmap.Status, Status = apiBeatmap.Status,
MD5Hash = apiBeatmap.MD5Hash MD5Hash = apiBeatmap.MD5Hash
}; };

View File

@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsButton new SettingsButton
{ {
Text = SkinSettingsStrings.SkinLayoutEditor, Text = SkinSettingsStrings.SkinLayoutEditor,
Action = () => skinEditor?.Toggle(), Action = () => skinEditor?.ToggleVisibility(),
}, },
new ExportSkinButton(), new ExportSkinButton(),
}; };

View File

@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI
/// </param> /// </param>
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam> /// <typeparam name="TObject">The <see cref="HitObject"/> type.</typeparam>
/// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam> /// <typeparam name="TDrawable">The <see cref="DrawableHitObject"/> receiver for <typeparamref name="TObject"/>s.</typeparam>
protected void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null) public void RegisterPool<TObject, TDrawable>(int initialSize, int? maximumSize = null)
where TObject : HitObject where TObject : HitObject
where TDrawable : DrawableHitObject, new() where TDrawable : DrawableHitObject, new()
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize)); => RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));

View File

@ -157,7 +157,7 @@ namespace osu.Game.Scoring
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy(); public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
/// <summary> /// <summary>
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score. /// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary> /// </summary>
[Ignored] [Ignored]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any(); public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();

View File

@ -134,36 +134,10 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash)) if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
return score.TotalScore; return score.TotalScore;
int beatmapMaxCombo; int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false);
if (beatmapMaxCombo == null)
if (score.IsLegacyScore)
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
if (score.BeatmapInfo.MaxCombo != null)
beatmapMaxCombo = score.BeatmapInfo.MaxCombo.Value;
else
{
if (difficulties == null)
return score.TotalScore; return score.TotalScore;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
// Something failed during difficulty calculation. Fall back to provided score.
if (difficulty == null)
return score.TotalScore;
beatmapMaxCombo = difficulty.Value.MaxCombo;
}
}
else
{
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
}
if (beatmapMaxCombo == 0) if (beatmapMaxCombo == 0)
return 0; return 0;
@ -171,7 +145,37 @@ namespace osu.Game.Scoring
var scoreProcessor = ruleset.CreateScoreProcessor(); var scoreProcessor = ruleset.CreateScoreProcessor();
scoreProcessor.Mods.Value = score.Mods; scoreProcessor.Mods.Value = score.Mods;
return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo)); return (long)Math.Round(scoreProcessor.ComputeFinalLegacyScore(mode, score, beatmapMaxCombo.Value));
}
/// <summary>
/// Retrieves the maximum achievable combo for the provided score.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to compute the maximum achievable combo for.</param>
/// <param name="cancellationToken">A <see cref="CancellationToken"/> to cancel the process.</param>
/// <returns>The maximum achievable combo. A <see langword="null"/> return value indicates the difficulty cache has failed to retrieve the combo.</returns>
public async Task<int?> GetMaximumAchievableComboAsync([NotNull] ScoreInfo score, CancellationToken cancellationToken = default)
{
if (score.IsLegacyScore)
{
// This score is guaranteed to be an osu!stable score.
// The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used.
#pragma warning disable CS0618
if (score.BeatmapInfo.MaxCombo != null)
return score.BeatmapInfo.MaxCombo.Value;
#pragma warning restore CS0618
if (difficulties == null)
return null;
// We can compute the max combo locally after the async beatmap difficulty computation.
var difficulty = await difficulties().GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, cancellationToken).ConfigureAwait(false);
return difficulty?.MaxCombo;
}
// This is guaranteed to be a non-legacy score.
// The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values.
return Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetValueOrDefault(r)).Sum();
} }
/// <summary> /// <summary>

View File

@ -65,10 +65,12 @@ namespace osu.Game.Screens.Ranking.Expanded
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
string creator = metadata.Author.Username; string creator = metadata.Author.Username;
int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely();
var topStatistics = new List<StatisticDisplay> var topStatistics = new List<StatisticDisplay>
{ {
new AccuracyStatistic(score.Accuracy), new AccuracyStatistic(score.Accuracy),
new ComboStatistic(score.MaxCombo, beatmap.MaxCombo, score.Statistics.All(stat => !stat.Key.BreaksCombo() || stat.Value == 0)), new ComboStatistic(score.MaxCombo, beatmapMaxCombo),
new PerformanceStatistic(score), new PerformanceStatistic(score),
}; };
@ -80,8 +82,6 @@ namespace osu.Game.Screens.Ranking.Expanded
statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics); statisticDisplays.AddRange(bottomStatistics);
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
AddInternal(new FillFlowContainer AddInternal(new FillFlowContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -224,6 +224,8 @@ namespace osu.Game.Screens.Ranking.Expanded
if (score.Date != default) if (score.Date != default)
AddInternal(new PlayedOnText(score.Date)); AddInternal(new PlayedOnText(score.Date));
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
if (starDifficulty != null) if (starDifficulty != null)
{ {
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value) starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)

View File

@ -26,11 +26,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
/// </summary> /// </summary>
/// <param name="combo">The combo to be displayed.</param> /// <param name="combo">The combo to be displayed.</param>
/// <param name="maxCombo">The maximum value of <paramref name="combo"/>.</param> /// <param name="maxCombo">The maximum value of <paramref name="combo"/>.</param>
/// <param name="isPerfect">Whether this is a perfect combo.</param> public ComboStatistic(int combo, int? maxCombo)
public ComboStatistic(int combo, int? maxCombo, bool isPerfect)
: base("combo", combo, maxCombo) : base("combo", combo, maxCombo)
{ {
this.isPerfect = isPerfect; isPerfect = combo == maxCombo;
} }
public override void Appear() public override void Appear()

View File

@ -19,15 +19,15 @@ namespace osu.Game.Skinning.Editor
/// A container which handles loading a skin editor on user request for a specified target. /// A container which handles loading a skin editor on user request for a specified target.
/// This also handles the scaling / positioning adjustment of the target. /// This also handles the scaling / positioning adjustment of the target.
/// </summary> /// </summary>
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction> public class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
{ {
private readonly ScalingContainer scalingContainer; private readonly ScalingContainer scalingContainer;
protected override bool BlockNonPositionalInput => true;
[CanBeNull] [CanBeNull]
private SkinEditor skinEditor; private SkinEditor skinEditor;
public const float VISIBLE_TARGET_SCALE = 0.8f;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private OsuGame game { get; set; } private OsuGame game { get; set; }
@ -49,33 +49,13 @@ namespace osu.Game.Skinning.Editor
Hide(); Hide();
return true; return true;
case GlobalAction.ToggleSkinEditor:
Toggle();
return true;
} }
return false; return false;
} }
public void Toggle() protected override void PopIn()
{ {
if (skinEditor == null)
Show();
else
skinEditor.ToggleVisibility();
}
public override void Hide()
{
// base call intentionally omitted.
skinEditor?.Hide();
}
public override void Show()
{
// base call intentionally omitted as we have custom behaviour.
if (skinEditor != null) if (skinEditor != null)
{ {
skinEditor.Show(); skinEditor.Show();
@ -83,17 +63,11 @@ namespace osu.Game.Skinning.Editor
} }
var editor = new SkinEditor(); var editor = new SkinEditor();
editor.State.BindValueChanged(visibility => updateComponentVisibility()); editor.State.BindValueChanged(visibility => updateComponentVisibility());
skinEditor = editor; skinEditor = editor;
// Schedule ensures that if `Show` is called before this overlay is loaded,
// it will not throw (LoadComponentAsync requires the load target to be in a loaded state).
Schedule(() =>
{
if (editor != skinEditor)
return;
LoadComponentAsync(editor, _ => LoadComponentAsync(editor, _ =>
{ {
if (editor != skinEditor) if (editor != skinEditor)
@ -103,9 +77,10 @@ namespace osu.Game.Skinning.Editor
SetTarget(lastTargetScreen); SetTarget(lastTargetScreen);
}); });
});
} }
protected override void PopOut() => skinEditor?.Hide();
private void updateComponentVisibility() private void updateComponentVisibility()
{ {
Debug.Assert(skinEditor != null); Debug.Assert(skinEditor != null);

View File

@ -104,7 +104,7 @@ namespace osu.Game.Skinning.Editor
}; };
} }
private class SceneButton : OsuButton public class SceneButton : OsuButton
{ {
public SceneButton() public SceneButton()
{ {

View File

@ -163,6 +163,12 @@ namespace osu.Game.Stores
return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds); return existing.OnlineID == import.OnlineID && existingIds.SequenceEqual(importIds);
} }
protected override void UndeleteForReuse(BeatmapSetInfo existing)
{
base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow;
}
public override bool IsAvailableLocally(BeatmapSetInfo model) public override bool IsAvailableLocally(BeatmapSetInfo model)
{ {
return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID)); return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));

View File

@ -351,7 +351,8 @@ namespace osu.Game.Stores
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {
existing.DeletePending = false; if (existing.DeletePending)
UndeleteForReuse(existing);
transaction.Commit(); transaction.Commit();
} }
@ -387,7 +388,9 @@ namespace osu.Game.Stores
{ {
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import."); LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
existing.DeletePending = false; if (existing.DeletePending)
UndeleteForReuse(existing);
transaction.Commit(); transaction.Commit();
return existing.ToLive(Realm); return existing.ToLive(Realm);
@ -527,6 +530,15 @@ namespace osu.Game.Stores
private bool checkAllFilesExist(TModel model) => private bool checkAllFilesExist(TModel model) =>
model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath())); model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath()));
/// <summary>
/// Called when an existing model is in a soft deleted state but being recovered.
/// </summary>
/// <param name="existing">The existing model.</param>
protected virtual void UndeleteForReuse(TModel existing)
{
existing.DeletePending = false;
}
/// <summary> /// <summary>
/// Whether this specified path should be removed after successful import. /// Whether this specified path should be removed after successful import.
/// </summary> /// </summary>