1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 08:52:56 +08:00

Merge branch 'master' into beatmapSelectionBlur

This commit is contained in:
Dean Herbert 2023-01-25 16:32:00 +09:00
commit 3faab66f39
32 changed files with 447 additions and 159 deletions

View File

@ -121,21 +121,12 @@ jobs:
build-only-ios: build-only-ios:
name: Build only (iOS) name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12. runs-on: macos-latest
runs-on: macos-12
timeout-minutes: 60 timeout-minutes: 60
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2
# see https://github.com/actions/runner-images/issues/6771#issuecomment-1354713617
# remove once all workflow VMs use Xcode 14.1
- name: Set Xcode Version
shell: bash
run: |
sudo xcode-select -s "/Applications/Xcode_14.1.app"
echo "MD_APPLE_SDK_ROOT=/Applications/Xcode_14.1.app" >> $GITHUB_ENV
- name: Install .NET 6.0.x - name: Install .NET 6.0.x
uses: actions/setup-dotnet@v1 uses: actions/setup-dotnet@v1
with: with:

View File

@ -113,6 +113,7 @@ namespace osu.Game.Rulesets.Catch
new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()), new MultiMod(new CatchModDoubleTime(), new CatchModNightcore()),
new CatchModHidden(), new CatchModHidden(),
new CatchModFlashlight(), new CatchModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -245,6 +245,7 @@ namespace osu.Game.Rulesets.Mania
new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()), new MultiMod(new ManiaModDoubleTime(), new ManiaModNightcore()),
new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()), new MultiMod(new ManiaModFadeIn(), new ManiaModHidden()),
new ManiaModFlashlight(), new ManiaModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary> /// </summary>
public Vector2 PathStartLocation => body.PathOffset; public Vector2 PathStartLocation => body.PathOffset;
/// <summary>
/// Offset in absolute (local) coordinates from the end of the curve.
/// </summary>
public Vector2 PathEndLocation => body.PathEndOffset;
public SliderBodyPiece() public SliderBodyPiece()
{ {
InternalChild = body = new ManualSliderBody InternalChild = body = new ManualSliderBody

View File

@ -409,6 +409,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset) public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathOffset)
?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); ?? BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation);
protected override Vector2[] ScreenSpaceAdditionalNodes => new[]
{
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
};
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;

View File

@ -0,0 +1,14 @@
// 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.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAccuracyChallenge : ModAccuracyChallenge
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
}
}

View File

@ -164,7 +164,8 @@ 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() new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]); public virtual Vector2 PathOffset => path.PositionInBoundingBox(path.Vertices[0]);
/// <summary>
/// Offset in absolute coordinates from the end of the curve.
/// </summary>
public virtual Vector2 PathEndOffset => path.PositionInBoundingBox(path.Vertices[^1]);
/// <summary> /// <summary>
/// Used to colour the path. /// Used to colour the path.
/// </summary> /// </summary>

View File

@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
public override Vector2 PathOffset => snakedPathOffset; public override Vector2 PathOffset => snakedPathOffset;
public override Vector2 PathEndOffset => snakedPathEndOffset;
/// <summary> /// <summary>
/// The top-left position of the path when fully snaked. /// The top-left position of the path when fully snaked.
/// </summary> /// </summary>
@ -53,6 +55,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary> /// </summary>
private Vector2 snakedPathOffset; private Vector2 snakedPathOffset;
/// <summary>
/// The offset of the end of path from <see cref="snakedPosition"/> when fully snaked.
/// </summary>
private Vector2 snakedPathEndOffset;
private DrawableSlider drawableSlider = null!; private DrawableSlider drawableSlider = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -109,6 +116,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero); snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]); snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
snakedPathEndOffset = Path.PositionInBoundingBox(Path.Vertices[^1]);
double lastSnakedStart = SnakedStart ?? 0; double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0; double lastSnakedEnd = SnakedEnd ?? 0;

View File

@ -10,6 +10,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges; using osu.Framework.Input.StateChanges;
using osu.Game.Configuration; using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI namespace osu.Game.Rulesets.Osu.UI
{ {
@ -38,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.UI
mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons); mouseDisabled = config.GetBindable<bool>(OsuSetting.MouseDisableButtons);
} }
// Required to handle touches outside of the playfield when screen scaling is enabled.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override void OnTouchMove(TouchMoveEvent e) protected override void OnTouchMove(TouchMoveEvent e)
{ {
base.OnTouchMove(e); base.OnTouchMove(e);

View File

@ -1,7 +1,6 @@
// 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.Diagnostics;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.Taiko.UI;
@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods namespace osu.Game.Rulesets.Taiko.Mods
{ {
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IUpdatableByPlayfield public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>
{ {
private DrawableTaikoRuleset? drawableTaikoRuleset;
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset) public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{ {
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset; var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false; drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield; var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true; playfield.ClassicHitTargetPosition.Value = true;
} }
public void Update(Playfield playfield)
{
Debug.Assert(drawableTaikoRuleset != null);
// Classic taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = drawableTaikoRuleset.DrawHeight / 480;
drawableTaikoRuleset.TimeRange.Value = (playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
} }
} }

View File

@ -144,6 +144,7 @@ namespace osu.Game.Rulesets.Taiko
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()), new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(), new TaikoModHidden(),
new TaikoModFlashlight(), new TaikoModFlashlight(),
new ModAccuracyChallenge(),
}; };
case ModType.Conversion: case ModType.Conversion:

View File

@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods) : base(ruleset, beatmap, mods)
{ {
Direction.Value = ScrollingDirection.Left; Direction.Value = ScrollingDirection.Left;
TimeRange.Value = 7000;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
KeyBindingInputManager.Add(new DrumTouchInputArea()); KeyBindingInputManager.Add(new DrumTouchInputArea());
} }
protected override void Update()
{
base.Update();
// Taiko scrolls at a constant 100px per 1000ms. More notes become visible as the playfield is lengthened.
const float scroll_rate = 10;
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
float ratio = DrawHeight / 480;
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
}
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved] [Resolved]
private BeatmapManager beatmapManager { get; set; } = null!; private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty; private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps() public override void SetUpSteps()
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName && beatmap.DifficultyName == secondDifficultyName
&& set != null && set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified)); && set.PerformRead(s =>
s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
}); });
} }
@ -327,6 +332,56 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2)); AddAssert("old beatmap file not deleted", () => refetchedBeatmapSet.AsNonNull().PerformRead(s => s.Files.Count == 2));
} }
[Test]
public void TestCopyDifficultyDoesNotChangeCollections()
{
string originalDifficultyName = Guid.NewGuid().ToString();
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = originalDifficultyName);
AddStep("save beatmap", () => Editor.Save());
string originalMd5 = string.Empty;
BeatmapCollection collection = null!;
AddStep("setup a collection with original beatmap", () =>
{
collection = new BeatmapCollection("test copy");
collection.BeatmapMD5Hashes.Add(originalMd5 = EditorBeatmap.BeatmapInfo.MD5Hash);
realm.Write(r =>
{
r.Add(collection);
});
});
AddAssert("collection contains original beatmap", () =>
!string.IsNullOrEmpty(originalMd5) && collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != originalDifficultyName;
});
AddStep("save without changes", () => Editor.Save());
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
&& collection.BeatmapMD5Hashes.Contains(originalMd5));
AddStep("clean up collection", () =>
{
realm.Write(r =>
{
r.Remove(collection);
});
});
}
[Test] [Test]
public void TestCreateMultipleNewDifficultiesSucceeds() public void TestCreateMultipleNewDifficultiesSucceeds()
{ {

View File

@ -123,7 +123,7 @@ needs_cleanup: true
AddStep("Add absolute image", () => AddStep("Add absolute image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh"; markdownContainer.CurrentPath = "https://dev.ppy.sh";
markdownContainer.Text = "![intro](/wiki/Interface/img/intro-screen.jpg)"; markdownContainer.Text = "![intro](/wiki/images/Client/Interface/img/intro-screen.jpg)";
}); });
} }
@ -133,7 +133,7 @@ needs_cleanup: true
AddStep("Add relative image", () => AddStep("Add relative image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = "![intro](img/intro-screen.jpg)"; markdownContainer.Text = "![intro](../images/Client/Interface/img/intro-screen.jpg)";
}); });
} }
@ -145,7 +145,7 @@ needs_cleanup: true
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/Interface/";
markdownContainer.Text = @"Line before image markdownContainer.Text = @"Line before image
![play menu](img/play-menu.jpg ""Main Menu in osu!"") ![play menu](../images/Client/Interface/img/play-menu.jpg ""Main Menu in osu!"")
Line after image"; Line after image";
}); });
@ -170,12 +170,12 @@ Line after image";
markdownContainer.Text = @" markdownContainer.Text = @"
| Image | Name | Effect | | Image | Name | Effect |
| :-: | :-: | :-- | | :-: | :-: | :-- |
| ![](/wiki/Skinning/Interface/img/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. | | ![](/wiki/images/shared/judgement/osu!/hit300.png ""300"") | 300 | A possible score when tapping a hit circle precisely on time, completing a Slider and keeping the cursor over every tick, or completing a Spinner with the Spinner Metre full. A score of 300 appears in an blue score by default. Scoring nothing except 300s in a beatmap will award the player with the SS or SSH grade. |
| ![](/wiki/Skinning/Interface/img/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. | | ![](/wiki/images/shared/judgement/osu!/hit300g.png ""Geki"") | () Geki | A term from Ouendan, called Elite Beat! in EBA. Appears when playing the last element in a combo in which the player has scored only 300s. Getting a Geki will give a sizable boost to the Life Bar. By default, it is blue. |
| ![](/wiki/Skinning/Interface/img/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. | | ![](/wiki/images/shared/judgement/osu!/hit100.png ""100"") | 100 | A possible score one can get when tapping a Hit Object slightly late or early, completing a Slider and missing a number of ticks, or completing a Spinner with the Spinner Meter almost full. A score of 100 appears in a green score by default. When very skilled players test a beatmap and they get a lot of 100s, this may mean that the beatmap does not have correct timing. |
| ![](/wiki/Skinning/Interface/img/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. | | ![](/wiki/images/shared/judgement/osu!/hit300k.png ""300 Katu"") ![](/wiki/Skinning/Interface/img/hit100k.png ""100 Katu"") | () Katu or Katsu | A term from Ouendan, called Beat! in EBA. Appears when playing the last element in a combo in which the player has scored at least one 100, but no 50s or misses. Getting a Katu will give a small boost to the Life Bar. By default, it is coloured green or blue depending on whether the Katu itself is a 100 or a 300. |
| ![](/wiki/Skinning/Interface/img/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. | | ![](/wiki/images/shared/judgement/osu!/hit50.png ""50"") | 50 | A possible score one can get when tapping a hit circle rather early or late but not early or late enough to cause a miss, completing a Slider and missing a lot of ticks, or completing a Spinner with the Spinner Metre close to full. A score of 50 appears in a orange score by default. Scoring a 50 in a combo will prevent the appearance of a Katu or a Geki at the combo's end. |
| ![](/wiki/Skinning/Interface/img/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. | | ![](/wiki/images/shared/judgement/osu!/hit0.png ""Miss"") | Miss | A possible score one can get when not tapping a hit circle or too early (based on OD and AR, it may *shake* instead), not tapping or holding the Slider at least once, or completing a Spinner with low Spinner Metre fill. Scoring a Miss will reset the current combo to 0 and will prevent the appearance of a Katu or a Geki at the combo's end. |
"; ";
}); });
} }
@ -186,7 +186,7 @@ Line after image";
AddStep("Add image", () => AddStep("Add image", () =>
{ {
markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/"; markdownContainer.CurrentPath = "https://dev.ppy.sh/wiki/osu!_Program_Files/";
markdownContainer.Text = "![](img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")"; markdownContainer.Text = "![](../images/Client/Program_files/img/file_structure.jpg \"The file structure of osu!'s installation folder, on Windows and macOS\")";
}); });
AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted); AddUntilStep("Wait image to load", () => markdownContainer.ChildrenOfType<DelayedLoadWrapper>().First().DelayedLoadCompleted);
@ -270,6 +270,30 @@ Phasellus eu nunc nec ligula semper fringilla. Aliquam magna neque, placerat sed
}); });
} }
[Test]
public void TestCodeSyntax()
{
AddStep("set content", () =>
{
markdownContainer.Text = @"
This is a paragraph containing `inline code` synatax.
Oh wow I do love the `WikiMarkdownContainer`, it is very cool!
This is a line before the fenced code block:
```csharp
public class WikiMarkdownContainer : MarkdownContainer
{
public WikiMarkdownContainer()
{
this.foo = bar;
}
}
```
This is a line after the fenced code block!
";
});
}
private partial class TestMarkdownContainer : WikiMarkdownContainer private partial class TestMarkdownContainer : WikiMarkdownContainer
{ {
public LinkInline Link; public LinkInline Link;

View File

@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo); targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet; newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin); save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet); workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo); return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap; public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
/// <summary> /// <summary>
/// Saves an <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>. /// Saves an existing <see cref="IBeatmap"/> file against a given <see cref="BeatmapInfo"/>.
/// </summary> /// </summary>
/// <remarks>
/// This method will also update any user beatmap collection hash references to the new post-saved hash.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param> /// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param> /// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param> /// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
{ save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
var setInfo = beatmapInfo.BeatmapSet;
Debug.Assert(setInfo != null);
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file.
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
});
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}
public void DeleteAllVideos() public void DeleteAllVideos()
{ {
@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
setInfo.Status = BeatmapOnlineStatus.LocallyModified; setInfo.Status = BeatmapOnlineStatus.LocallyModified;
} }
private void save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin, bool transferCollections)
{
var setInfo = beatmapInfo.BeatmapSet;
Debug.Assert(setInfo != null);
// Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`.
// This should hopefully be temporary, assuming said clone is eventually removed.
// Warning: The directionality here is important. Changes have to be copied *from* beatmapContent (which comes from editor and is being saved)
// *to* the beatmapInfo (which is a database model and needs to receive values without the taiko slider velocity multiplier for correct operation).
// CopyTo() will undo such adjustments, while CopyFrom() will not.
beatmapContent.Difficulty.CopyTo(beatmapInfo.Difficulty);
// All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding.
beatmapContent.BeatmapInfo = beatmapInfo;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file.
if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException($"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.");
if (existingFileInfo != null)
DeleteFile(setInfo, existingFileInfo);
string oldMd5Hash = beatmapInfo.MD5Hash;
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
updateHashAndMarkDirty(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
ProcessBeatmap?.Invoke((liveBeatmapSet, false));
});
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
static string createBeatmapFilenameFromMetadata(BeatmapInfo beatmapInfo)
{
var metadata = beatmapInfo.Metadata;
return $"{metadata.Artist} - {metadata.Title} ({metadata.Author.Username}) [{beatmapInfo.DifficultyName}].osu".GetValidFilename();
}
}
#region Implementation of ICanAcceptFiles #region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths); public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

@ -556,8 +556,8 @@ namespace osu.Game
case JoystickHandler jh: case JoystickHandler jh:
return new JoystickSettings(jh); return new JoystickSettings(jh);
case TouchHandler: case TouchHandler th:
return new InputSection.HandlerSection(handler); return new TouchSettings(th);
} }
} }

View File

@ -5,12 +5,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -22,8 +24,6 @@ namespace osu.Game.Overlays
{ {
public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader> public partial class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
{ {
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>(); public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private List<APIChangelogBuild> builds; private List<APIChangelogBuild> builds;
@ -81,6 +81,8 @@ namespace osu.Game.Overlays
ArgumentNullException.ThrowIfNull(updateStream); ArgumentNullException.ThrowIfNull(updateStream);
ArgumentNullException.ThrowIfNull(version); ArgumentNullException.ThrowIfNull(version);
Show();
performAfterFetch(() => performAfterFetch(() =>
{ {
var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream) var build = builds.Find(b => b.Version == version && b.UpdateStream.Name == updateStream)
@ -89,8 +91,6 @@ namespace osu.Game.Overlays
if (build != null) if (build != null)
ShowBuild(build); ShowBuild(build);
}); });
Show();
} }
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
@ -127,11 +127,16 @@ namespace osu.Game.Overlays
private Task initialFetchTask; private Task initialFetchTask;
private void performAfterFetch(Action action) => Schedule(() => private void performAfterFetch(Action action)
{ {
fetchListing()?.ContinueWith(_ => Debug.Assert(State.Value == Visibility.Visible);
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
}); Schedule(() =>
{
fetchListing()?.ContinueWith(_ =>
Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion);
});
}
private Task fetchListing() private Task fetchListing()
{ {

View File

@ -35,6 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
new SettingsCheckbox new SettingsCheckbox
{ {
LabelText = SkinSettingsStrings.GameplayCursorDuringTouch, LabelText = SkinSettingsStrings.GameplayCursorDuringTouch,
Keywords = new[] { @"touchscreen" },
Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch) Current = config.GetBindable<bool>(OsuSetting.GameplayCursorDuringTouch)
}, },
}; };

View File

@ -70,6 +70,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
Add(new SettingsButton Add(new SettingsButton
{ {
Text = GeneralSettingsStrings.OpenOsuFolder, Text = GeneralSettingsStrings.OpenOsuFolder,
Keywords = new[] { @"logs", @"files", @"access", "directory" },
Action = () => storage.PresentExternally(), Action = () => storage.PresentExternally(),
}); });

View File

@ -34,6 +34,7 @@ namespace osu.Game.Overlays.Settings.Sections
new SettingsButton new SettingsButton
{ {
Text = GeneralSettingsStrings.RunSetupWizard, Text = GeneralSettingsStrings.RunSetupWizard,
Keywords = new[] { @"first run", @"initial", @"getting started" },
TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription, TooltipText = FirstRunSetupOverlayStrings.FirstRunSetupDescription,
Action = () => firstRunSetupOverlay?.Show(), Action = () => firstRunSetupOverlay?.Show(),
}, },

View File

@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings; protected override LocalisableString Header => BindingSettingsStrings.ShortcutAndGameplayBindings;
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "keybindings" }); public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"keybindings", @"controls", @"keyboard", @"keys" });
public BindingSettings(KeyBindingPanel keyConfig) public BindingSettings(KeyBindingPanel keyConfig)
{ {

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Handlers.Touch;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Input
{
public partial class TouchSettings : SettingsSubsection
{
private readonly TouchHandler handler;
public TouchSettings(TouchHandler handler)
{
this.handler = handler;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new SettingsCheckbox
{
LabelText = CommonStrings.Enabled,
Current = handler.Enabled
},
};
}
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { @"touchscreen" });
protected override LocalisableString Header => handler.Description;
}
}

View File

@ -1,8 +1,8 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
public BindableBool CanBeShown { get; } = new BindableBool(true); public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown; IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
get get
{ {
if (TooltipText != default) if (TooltipText != default)
return base.FilterTerms.Append(TooltipText); yield return TooltipText;
return base.FilterTerms; foreach (string s in Keywords)
yield return s;
foreach (LocalisableString s in base.FilterTerms)
yield return s;
} }
} }
} }

View File

@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Settings
Text = game.Name, Text = game.Name,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
}, },
new BuildDisplay(game.Version, DebugUtils.IsDebugBuild) new BuildDisplay(game.Version)
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
private partial class BuildDisplay : OsuAnimatedButton private partial class BuildDisplay : OsuAnimatedButton
{ {
private readonly string version; private readonly string version;
private readonly bool isDebug;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
public BuildDisplay(string version, bool isDebug) public BuildDisplay(string version)
{ {
this.version = version; this.version = version;
this.isDebug = isDebug;
Content.RelativeSizeAxes = Axes.Y; Content.RelativeSizeAxes = Axes.Y;
Content.AutoSizeAxes = AutoSizeAxes = Axes.X; Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(ChangelogOverlay changelog) private void load(ChangelogOverlay changelog)
{ {
if (!isDebug) Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
Action = () => changelog?.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
Add(new OsuSpriteText Add(new OsuSpriteText
{ {
@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Padding = new MarginPadding(5), Padding = new MarginPadding(5),
Colour = isDebug ? colours.Red : Color4.White, Colour = DebugUtils.IsDebugBuild ? colours.Red : Color4.White,
}); });
} }
} }

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using osu.Framework; using osu.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>(); public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <summary> /// <summary>
/// The screen-space point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag. /// The screen-space main point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
/// </summary> /// </summary>
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre; public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
/// <summary>
/// Any points that should be used for snapping purposes in addition to <see cref="ScreenSpaceSelectionPoint"/>. Exposed via <see cref="ScreenSpaceSnapPoints"/>.
/// </summary>
protected virtual Vector2[] ScreenSpaceAdditionalNodes => Array.Empty<Vector2>();
/// <summary>
/// The screen-space collection of base points on this <see cref="HitObjectSelectionBlueprint"/> that other objects can be snapped to.
/// The first element of this collection is <see cref="ScreenSpaceSelectionPoint"/>
/// </summary>
public Vector2[] ScreenSpaceSnapPoints => ScreenSpaceAdditionalNodes.Prepend(ScreenSpaceSelectionPoint).ToArray();
/// <summary> /// <summary>
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections. /// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
/// </summary> /// </summary>

View File

@ -0,0 +1,80 @@
// 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.Globalization;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Judgements;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public class ModAccuracyChallenge : ModFailCondition, IApplicableToScoreProcessor
{
public override string Name => "Accuracy Challenge";
public override string Acronym => "AC";
public override LocalisableString Description => "Fail if your accuracy drops too low!";
public override ModType Type => ModType.DifficultyIncrease;
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModEasyWithExtraLives), typeof(ModPerfect) }).ToArray();
public override bool RequiresConfiguration => false;
public override string SettingDescription => base.SettingDescription.Replace(MinimumAccuracy.ToString(), MinimumAccuracy.Value.ToString("##%", NumberFormatInfo.InvariantInfo));
[SettingSource("Minimum accuracy", "Trigger a failure if your accuracy goes below this value.", SettingControlType = typeof(SettingsSlider<double, PercentSlider>))]
public BindableNumber<double> MinimumAccuracy { get; } = new BindableDouble
{
MinValue = 0.60,
MaxValue = 0.99,
Precision = 0.01,
Default = 0.9,
Value = 0.9,
};
private ScoreProcessor scoreProcessor = null!;
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) => this.scoreProcessor = scoreProcessor;
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
{
if (!result.Type.AffectsAccuracy())
return false;
return getAccuracyWithImminentResultAdded(result) < MinimumAccuracy.Value;
}
private double getAccuracyWithImminentResultAdded(JudgementResult result)
{
var score = new ScoreInfo { Ruleset = scoreProcessor.Ruleset.RulesetInfo };
// This is super ugly, but if we don't do it this way we will not have the most recent result added to the accuracy value.
// Hopefully we can improve this in the future.
scoreProcessor.PopulateScore(score);
score.Statistics[result.Type]++;
return scoreProcessor.ComputeAccuracy(score);
}
}
public partial class PercentSlider : OsuSliderBar<double>
{
public PercentSlider()
{
DisplayAsPercentage = true;
}
}
}

View File

@ -1,6 +1,8 @@
// 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 Humanizer; using Humanizer;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -19,6 +21,7 @@ namespace osu.Game.Rulesets.Mods
}; };
public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries; private int retries;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override LocalisableString Description => "SS or quit."; public override LocalisableString Description => "SS or quit.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(ModSuddenDeath), typeof(ModAccuracyChallenge) }).ToArray();
protected ModPerfect() protected ModPerfect()
{ {

View File

@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary> /// </summary>
protected virtual double ClassicScoreMultiplier => 36; protected virtual double ClassicScoreMultiplier => 36;
private readonly Ruleset ruleset; /// <summary>
/// The ruleset this score processor is valid for.
/// </summary>
public readonly Ruleset Ruleset;
private readonly double accuracyPortion; private readonly double accuracyPortion;
private readonly double comboPortion; private readonly double comboPortion;
@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
public ScoreProcessor(Ruleset ruleset) public ScoreProcessor(Ruleset ruleset)
{ {
this.ruleset = ruleset; Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion; accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion; comboPortion = DefaultComboPortion;
@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure] [Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo) public double ComputeAccuracy(ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
// We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap. // We only extract scoring values from the score's statistics. This is because accuracy is always relative to the point of pass or fail rather than relative to the whole beatmap.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum); extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure] [Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo) public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{ {
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset)) if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\"."); throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum); extractScoringValues(scoreInfo, out var current, out var maximum);
@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring
break; break;
default: default:
maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result; maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
break; break;
} }

View File

@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement #region Selection Movement
private Vector2[] movementBlueprintOriginalPositions; private Vector2[][] movementBlueprintsOriginalPositions;
private SelectionBlueprint<T>[] movementBlueprints; private SelectionBlueprint<T>[] movementBlueprints;
private bool isDraggingBlueprint; private bool isDraggingBlueprint;
@ -459,7 +459,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item // Movement is tracked from the blueprint of the earliest item, since it only makes sense to distance snap from that item
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray(); movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
return true; return true;
} }
@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
Debug.Assert(movementBlueprintOriginalPositions != null); Debug.Assert(movementBlueprintsOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
if (snapProvider != null) if (snapProvider != null)
{ {
// check for positional snap for every object in selection (for things like object-object snapping) for (int i = 0; i < movementBlueprints.Length; i++)
for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++)
{ {
Vector2 originalPosition = movementBlueprintOriginalPositions[i]; if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - movementBlueprints[i].ScreenSpaceSelectionPoint;
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(movementBlueprints[i], delta)))
return true; return true;
} }
} }
@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// item in the selection. // item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition. // The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
// Retrieve a snapped position. // Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects); var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
return ApplySnapResult(movementBlueprints, result); return ApplySnapResult(movementBlueprints, result);
} }
/// <summary>
/// Check for positional snap for given blueprint.
/// </summary>
/// <param name="blueprint">The blueprint to check for snapping.</param>
/// <param name="distanceTravelled">Distance travelled since start of dragging action.</param>
/// <param name="originalPositions">The snap positions of blueprint before start of dragging action.</param>
/// <returns>Whether an object to snap to was found.</returns>
private bool checkSnappingBlueprintToNearbyObjects(SelectionBlueprint<T> blueprint, Vector2 distanceTravelled, Vector2[] originalPositions)
{
var currentPositions = blueprint.ScreenSpaceSnapPoints;
for (int i = 0; i < originalPositions.Length; i++)
{
Vector2 originalPosition = originalPositions[i];
var testPosition = originalPosition + distanceTravelled;
var positionalResult = snapProvider.FindSnappedPositionAndTime(testPosition, SnapType.NearbyObjects);
if (positionalResult.ScreenSpacePosition == testPosition) continue;
var delta = positionalResult.ScreenSpacePosition - currentPositions[i];
// attempt to move the objects, and abort any time based snapping if we can.
if (SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprint, delta)))
return true;
}
return false;
}
protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) => protected virtual bool ApplySnapResult(SelectionBlueprint<T>[] blueprints, SnapResult result) =>
SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint)); SelectionHandler.HandleMovement(new MoveSelectionEvent<T>(blueprints.First(), result.ScreenSpacePosition - blueprints.First().ScreenSpaceSelectionPoint));
@ -533,7 +552,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null) if (movementBlueprints == null)
return false; return false;
movementBlueprintOriginalPositions = null; movementBlueprintsOriginalPositions = null;
movementBlueprints = null; movementBlueprints = null;
return true; return true;

View File

@ -104,6 +104,9 @@ namespace osu.Game.Screens.Select.Leaderboards
protected override APIRequest? FetchScores(CancellationToken cancellationToken) protected override APIRequest? FetchScores(CancellationToken cancellationToken)
{ {
scoreRetrievalRequest?.Cancel();
scoreRetrievalRequest = null;
var fetchBeatmapInfo = BeatmapInfo; var fetchBeatmapInfo = BeatmapInfo;
if (fetchBeatmapInfo == null) if (fetchBeatmapInfo == null)
@ -152,8 +155,6 @@ namespace osu.Game.Screens.Select.Leaderboards
else if (filterMods) else if (filterMods)
requestMods = mods.Value; requestMods = mods.Value;
scoreRetrievalRequest?.Cancel();
var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods); var newRequest = new GetScoresRequest(fetchBeatmapInfo, fetchRuleset, Scope, requestMods);
newRequest.Success += response => Schedule(() => newRequest.Success += response => Schedule(() =>
{ {