mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 08:13:31 +08:00
Merge branch 'master' into hold-to-discard-changes
This commit is contained in:
commit
8647bebdca
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-dotnettools.csharp"
|
||||
]
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Configuration;
|
||||
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
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.")]
|
||||
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
|
||||
|
||||
|
148
osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
Normal file
148
osu.Game.Rulesets.Osu/Mods/OsuModStrictTracking.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, __) => updateNestedSamples();
|
||||
SamplesBindable.CollectionChanged += (_, __) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
}
|
||||
|
||||
@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
}
|
||||
}
|
||||
|
||||
updateNestedSamples();
|
||||
UpdateNestedSamples();
|
||||
}
|
||||
|
||||
private void updateNestedPositions()
|
||||
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
TailCircle.Position = EndPosition;
|
||||
}
|
||||
|
||||
private void updateNestedSamples()
|
||||
protected void UpdateNestedSamples()
|
||||
{
|
||||
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)
|
||||
|
@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new MultiMod(new OsuModDoubleTime(), new OsuModNightcore()),
|
||||
new OsuModHidden(),
|
||||
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
|
||||
new OsuModStrictTracking()
|
||||
};
|
||||
|
||||
case ModType.Conversion:
|
||||
|
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
switch (osuComponent.Component)
|
||||
{
|
||||
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:
|
||||
var followCircle = this.GetAnimation("sliderfollowcircle", true, true, true);
|
||||
|
@ -590,6 +590,8 @@ namespace osu.Game.Tests.Database
|
||||
|
||||
Assert.IsTrue(imported.DeletePending);
|
||||
|
||||
var originalAddedDate = imported.DateAdded;
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
// 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.IsFalse(imported.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);
|
||||
|
||||
var originalAddedDate = imported.DateAdded;
|
||||
|
||||
var importedSecondTime = await LoadOszIntoStore(importer, realm.Realm);
|
||||
|
||||
// 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.IsFalse(imported.DeletePending);
|
||||
Assert.IsFalse(importedSecondTime.DeletePending);
|
||||
Assert.That(importedSecondTime.DateAdded, Is.GreaterThan(originalAddedDate));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -133,7 +133,6 @@ namespace osu.Game.Tests.Resources
|
||||
StarRating = diff,
|
||||
Length = length,
|
||||
BPM = bpm,
|
||||
MaxCombo = 1000,
|
||||
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
|
||||
Ruleset = rulesetInfo,
|
||||
Metadata = metadata,
|
||||
|
@ -1,14 +1,19 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
@ -29,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("reload skin editor", () =>
|
||||
{
|
||||
skinEditor?.Expire();
|
||||
Player.ScaleTo(0.8f);
|
||||
Player.ScaleTo(0.4f);
|
||||
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
|
||||
});
|
||||
}
|
||||
@ -40,6 +45,36 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.Leaderboards;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Overlays.Toolbar;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
@ -21,10 +23,12 @@ using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD.HitErrorMeters;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Leaderboards;
|
||||
using osu.Game.Screens.Select.Options;
|
||||
using osu.Game.Skinning.Editor;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
@ -66,6 +70,73 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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]
|
||||
public void TestRetryCountIncrements()
|
||||
{
|
||||
@ -120,7 +191,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
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);
|
||||
|
||||
@ -152,7 +224,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
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);
|
||||
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using Realms;
|
||||
@ -169,7 +170,12 @@ namespace osu.Game.Beatmaps
|
||||
[Ignored]
|
||||
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]
|
||||
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
|
||||
public int? MaxCombo { get; set; }
|
||||
|
||||
[Ignored]
|
||||
|
@ -53,9 +53,6 @@ namespace osu.Game.Beatmaps
|
||||
[NotMapped]
|
||||
public APIBeatmap OnlineInfo { get; set; }
|
||||
|
||||
[NotMapped]
|
||||
public int? MaxCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The playable length in milliseconds of this beatmap.
|
||||
/// </summary>
|
||||
|
@ -295,7 +295,6 @@ namespace osu.Game.Database
|
||||
TimelineZoom = beatmap.TimelineZoom,
|
||||
Countdown = beatmap.Countdown,
|
||||
CountdownOffset = beatmap.CountdownOffset,
|
||||
MaxCombo = beatmap.MaxCombo,
|
||||
Bookmarks = beatmap.Bookmarks,
|
||||
BeatmapSet = realmBeatmapSet,
|
||||
};
|
||||
|
@ -1046,6 +1046,10 @@ namespace osu.Game
|
||||
|
||||
switch (e.Action)
|
||||
{
|
||||
case GlobalAction.ToggleSkinEditor:
|
||||
skinEditor.ToggleVisibility();
|
||||
return true;
|
||||
|
||||
case GlobalAction.ResetInputSettings:
|
||||
Host.ResetInputHandlers();
|
||||
frameworkConfig.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode).SetDefault();
|
||||
|
@ -173,7 +173,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
{
|
||||
Text = score.MaxCombo.ToLocalisableString(@"0\x"),
|
||||
Font = OsuFont.GetFont(size: text_size),
|
||||
#pragma warning disable 618
|
||||
Colour = score.MaxCombo == score.BeatmapInfo.MaxCombo ? highAccuracyColour : Color4.White
|
||||
#pragma warning restore 618
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -78,7 +78,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
// TODO: temporary. should be removed once `OrderByTotalScore` can accept `IScoreInfo`.
|
||||
var beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
#pragma warning disable 618
|
||||
MaxCombo = apiBeatmap.MaxCombo,
|
||||
#pragma warning restore 618
|
||||
Status = apiBeatmap.Status,
|
||||
MD5Hash = apiBeatmap.MD5Hash
|
||||
};
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
new SettingsButton
|
||||
{
|
||||
Text = SkinSettingsStrings.SkinLayoutEditor,
|
||||
Action = () => skinEditor?.Toggle(),
|
||||
Action = () => skinEditor?.ToggleVisibility(),
|
||||
},
|
||||
new ExportSkinButton(),
|
||||
};
|
||||
|
@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </param>
|
||||
/// <typeparam name="TObject">The <see cref="HitObject"/> type.</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 TDrawable : DrawableHitObject, new()
|
||||
=> RegisterPool<TObject, TDrawable>(new DrawablePool<TDrawable>(initialSize, maximumSize));
|
||||
|
@ -157,7 +157,7 @@ namespace osu.Game.Scoring
|
||||
public LocalisableString DisplayAccuracy => Accuracy.FormatAccuracy();
|
||||
|
||||
/// <summary>
|
||||
/// Whether this <see cref="EFScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
|
||||
/// </summary>
|
||||
[Ignored]
|
||||
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
|
||||
|
@ -134,35 +134,9 @@ namespace osu.Game.Scoring
|
||||
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
||||
return score.TotalScore;
|
||||
|
||||
int beatmapMaxCombo;
|
||||
|
||||
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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
int? beatmapMaxCombo = await GetMaximumAchievableComboAsync(score, cancellationToken).ConfigureAwait(false);
|
||||
if (beatmapMaxCombo == null)
|
||||
return score.TotalScore;
|
||||
|
||||
if (beatmapMaxCombo == 0)
|
||||
return 0;
|
||||
@ -171,7 +145,37 @@ namespace osu.Game.Scoring
|
||||
var scoreProcessor = ruleset.CreateScoreProcessor();
|
||||
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>
|
||||
|
@ -65,10 +65,12 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
|
||||
string creator = metadata.Author.Username;
|
||||
|
||||
int? beatmapMaxCombo = scoreManager.GetMaximumAchievableComboAsync(score).GetResultSafely();
|
||||
|
||||
var topStatistics = new List<StatisticDisplay>
|
||||
{
|
||||
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),
|
||||
};
|
||||
|
||||
@ -80,8 +82,6 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
statisticDisplays.AddRange(topStatistics);
|
||||
statisticDisplays.AddRange(bottomStatistics);
|
||||
|
||||
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
|
||||
|
||||
AddInternal(new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
@ -224,6 +224,8 @@ namespace osu.Game.Screens.Ranking.Expanded
|
||||
if (score.Date != default)
|
||||
AddInternal(new PlayedOnText(score.Date));
|
||||
|
||||
var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).GetResultSafely();
|
||||
|
||||
if (starDifficulty != null)
|
||||
{
|
||||
starAndModDisplay.Add(new StarRatingDisplay(starDifficulty.Value)
|
||||
|
@ -26,11 +26,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
|
||||
/// </summary>
|
||||
/// <param name="combo">The combo to be displayed.</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, bool isPerfect)
|
||||
public ComboStatistic(int combo, int? maxCombo)
|
||||
: base("combo", combo, maxCombo)
|
||||
{
|
||||
this.isPerfect = isPerfect;
|
||||
isPerfect = combo == maxCombo;
|
||||
}
|
||||
|
||||
public override void Appear()
|
||||
|
@ -19,15 +19,15 @@ namespace osu.Game.Skinning.Editor
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class SkinEditorOverlay : CompositeDrawable, IKeyBindingHandler<GlobalAction>
|
||||
public class SkinEditorOverlay : OverlayContainer, IKeyBindingHandler<GlobalAction>
|
||||
{
|
||||
private readonly ScalingContainer scalingContainer;
|
||||
|
||||
protected override bool BlockNonPositionalInput => true;
|
||||
|
||||
[CanBeNull]
|
||||
private SkinEditor skinEditor;
|
||||
|
||||
public const float VISIBLE_TARGET_SCALE = 0.8f;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private OsuGame game { get; set; }
|
||||
|
||||
@ -49,33 +49,13 @@ namespace osu.Game.Skinning.Editor
|
||||
|
||||
Hide();
|
||||
return true;
|
||||
|
||||
case GlobalAction.ToggleSkinEditor:
|
||||
Toggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
skinEditor.Show();
|
||||
@ -83,29 +63,24 @@ namespace osu.Game.Skinning.Editor
|
||||
}
|
||||
|
||||
var editor = new SkinEditor();
|
||||
|
||||
editor.State.BindValueChanged(visibility => updateComponentVisibility());
|
||||
|
||||
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(() =>
|
||||
LoadComponentAsync(editor, _ =>
|
||||
{
|
||||
if (editor != skinEditor)
|
||||
return;
|
||||
|
||||
LoadComponentAsync(editor, _ =>
|
||||
{
|
||||
if (editor != skinEditor)
|
||||
return;
|
||||
AddInternal(editor);
|
||||
|
||||
AddInternal(editor);
|
||||
|
||||
SetTarget(lastTargetScreen);
|
||||
});
|
||||
SetTarget(lastTargetScreen);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void PopOut() => skinEditor?.Hide();
|
||||
|
||||
private void updateComponentVisibility()
|
||||
{
|
||||
Debug.Assert(skinEditor != null);
|
||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Skinning.Editor
|
||||
};
|
||||
}
|
||||
|
||||
private class SceneButton : OsuButton
|
||||
public class SceneButton : OsuButton
|
||||
{
|
||||
public SceneButton()
|
||||
{
|
||||
|
@ -163,6 +163,12 @@ namespace osu.Game.Stores
|
||||
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)
|
||||
{
|
||||
return Realm.Run(realm => realm.All<BeatmapSetInfo>().Any(s => s.OnlineID == model.OnlineID));
|
||||
|
@ -351,7 +351,8 @@ namespace osu.Game.Stores
|
||||
|
||||
using (var transaction = realm.BeginWrite())
|
||||
{
|
||||
existing.DeletePending = false;
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
@ -387,7 +388,9 @@ namespace osu.Game.Stores
|
||||
{
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
|
||||
existing.DeletePending = false;
|
||||
if (existing.DeletePending)
|
||||
UndeleteForReuse(existing);
|
||||
|
||||
transaction.Commit();
|
||||
|
||||
return existing.ToLive(Realm);
|
||||
@ -527,6 +530,15 @@ namespace osu.Game.Stores
|
||||
private bool checkAllFilesExist(TModel model) =>
|
||||
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>
|
||||
/// Whether this specified path should be removed after successful import.
|
||||
/// </summary>
|
||||
|
Loading…
Reference in New Issue
Block a user