1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-14 20:33:09 +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:
name: Build only (iOS)
# change to macos-latest once GitHub finishes migrating all repositories to macOS 12.
runs-on: macos-12
runs-on: macos-latest
timeout-minutes: 60
steps:
- name: Checkout
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
uses: actions/setup-dotnet@v1
with:

View File

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

View File

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

View File

@ -22,6 +22,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
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()
{
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)
?? 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) =>
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 OsuModHidden(),
new MultiMod(new OsuModFlashlight(), new OsuModBlinds()),
new OsuModStrictTracking()
new OsuModStrictTracking(),
new OsuModAccuracyChallenge(),
};
case ModType.Conversion:

View File

@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Skinning
/// </summary>
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>
/// Used to colour the path.
/// </summary>

View File

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

View File

@ -10,6 +10,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
@ -38,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.UI
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)
{
base.OnTouchMove(e);

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.UI;
@ -9,30 +8,15 @@ using osu.Game.Rulesets.UI;
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)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
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 TaikoModHidden(),
new TaikoModFlashlight(),
new ModAccuracyChallenge(),
};
case ModType.Conversion:

View File

@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Taiko.UI
: base(ruleset, beatmap, mods)
{
Direction.Value = ScrollingDirection.Left;
TimeRange.Value = 7000;
}
[BackgroundDependencyLoader]
@ -60,6 +59,19 @@ namespace osu.Game.Rulesets.Taiko.UI
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()
{
base.UpdateAfterChildren();

View File

@ -13,6 +13,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
@ -42,6 +43,9 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved]
private BeatmapManager beatmapManager { get; set; } = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps()
@ -224,7 +228,8 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
&& 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));
}
[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]
public void TestCreateMultipleNewDifficultiesSucceeds()
{

View File

@ -123,7 +123,7 @@ needs_cleanup: true
AddStep("Add absolute image", () =>
{
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", () =>
{
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.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";
});
@ -170,12 +170,12 @@ Line after image";
markdownContainer.Text = @"
| 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/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/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/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/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/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!/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!/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!/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!/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!/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!/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", () =>
{
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);
@ -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
{
public LinkInline Link;

View File

@ -186,7 +186,7 @@ namespace osu.Game.Beatmaps
targetBeatmapSet.Beatmaps.Add(newBeatmap.BeatmapInfo);
newBeatmap.BeatmapInfo.BeatmapSet = targetBeatmapSet;
Save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin);
save(newBeatmap.BeatmapInfo, newBeatmap, beatmapSkin, transferCollections: false);
workingBeatmapCache.Invalidate(targetBeatmapSet);
return GetWorkingBeatmap(newBeatmap.BeatmapInfo);
@ -280,77 +280,16 @@ namespace osu.Game.Beatmaps
public IWorkingBeatmap DefaultBeatmap => workingBeatmapCache.DefaultBeatmap;
/// <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>
/// <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="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>
public virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null)
{
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 virtual void Save(BeatmapInfo beatmapInfo, IBeatmap beatmapContent, ISkin? beatmapSkin = null) =>
save(beatmapInfo, beatmapContent, beatmapSkin, transferCollections: true);
public void DeleteAllVideos()
{
@ -460,6 +399,74 @@ namespace osu.Game.Beatmaps
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
public Task Import(params string[] paths) => beatmapImporter.Import(paths);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{
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)
{

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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -22,6 +22,8 @@ namespace osu.Game.Overlays.Settings
public LocalisableString TooltipText { get; set; }
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
@ -30,9 +32,13 @@ namespace osu.Game.Overlays.Settings
get
{
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,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
},
new BuildDisplay(game.Version, DebugUtils.IsDebugBuild)
new BuildDisplay(game.Version)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
@ -81,15 +81,13 @@ namespace osu.Game.Overlays.Settings
private partial class BuildDisplay : OsuAnimatedButton
{
private readonly string version;
private readonly bool isDebug;
[Resolved]
private OsuColour colours { get; set; }
public BuildDisplay(string version, bool isDebug)
public BuildDisplay(string version)
{
this.version = version;
this.isDebug = isDebug;
Content.RelativeSizeAxes = Axes.Y;
Content.AutoSizeAxes = AutoSizeAxes = Axes.X;
@ -99,8 +97,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader(true)]
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
{
@ -110,7 +107,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
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
using System;
using System.Linq;
using osu.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -125,10 +126,21 @@ namespace osu.Game.Rulesets.Edit
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
/// <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>
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>
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
/// </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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using Humanizer;
using osu.Framework.Bindables;
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 Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModAccuracyChallenge)).ToArray();
private int retries;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
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()
{

View File

@ -97,7 +97,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
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 comboPortion;
@ -145,7 +149,7 @@ namespace osu.Game.Rulesets.Scoring
public ScoreProcessor(Ruleset ruleset)
{
this.ruleset = ruleset;
Ruleset = ruleset;
accuracyPortion = DefaultAccuracyPortion;
comboPortion = DefaultComboPortion;
@ -291,8 +295,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure]
public double ComputeAccuracy(ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
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.
extractScoringValues(scoreInfo.Statistics, out var current, out var maximum);
@ -312,8 +316,8 @@ namespace osu.Game.Rulesets.Scoring
[Pure]
public long ComputeScore(ScoringMode mode, ScoreInfo scoreInfo)
{
if (!ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
if (!Ruleset.RulesetInfo.Equals(scoreInfo.Ruleset))
throw new ArgumentException($"Unexpected score ruleset. Expected \"{Ruleset.RulesetInfo.ShortName}\" but was \"{scoreInfo.Ruleset.ShortName}\".");
extractScoringValues(scoreInfo, out var current, out var maximum);
@ -552,7 +556,7 @@ namespace osu.Game.Rulesets.Scoring
break;
default:
maxResult = maxBasicResult ??= ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
maxResult = maxBasicResult ??= Ruleset.GetHitResults().MaxBy(kvp => Judgement.ToNumericResult(kvp.result)).result;
break;
}

View File

@ -439,7 +439,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Selection Movement
private Vector2[] movementBlueprintOriginalPositions;
private Vector2[][] movementBlueprintsOriginalPositions;
private SelectionBlueprint<T>[] movementBlueprints;
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
movementBlueprints = SortForMovement(SelectionHandler.SelectedBlueprints).ToArray();
movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray();
movementBlueprintsOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSnapPoints).ToArray();
return true;
}
@ -480,26 +480,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (movementBlueprints == null)
return false;
Debug.Assert(movementBlueprintOriginalPositions != null);
Debug.Assert(movementBlueprintsOriginalPositions != null);
Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
if (snapProvider != null)
{
// check for positional snap for every object in selection (for things like object-object snapping)
for (int i = 0; i < movementBlueprintOriginalPositions.Length; i++)
for (int i = 0; i < movementBlueprints.Length; i++)
{
Vector2 originalPosition = movementBlueprintOriginalPositions[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)))
if (checkSnappingBlueprintToNearbyObjects(movementBlueprints[i], distanceTravelled, movementBlueprintsOriginalPositions[i]))
return true;
}
}
@ -508,7 +497,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// item in the selection.
// The final movement position, relative to movementBlueprintOriginalPosition.
Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled;
Vector2 movePosition = movementBlueprintsOriginalPositions.First().First() + distanceTravelled;
// Retrieve a snapped position.
var result = snapProvider?.FindSnappedPositionAndTime(movePosition, ~SnapType.NearbyObjects);
@ -521,6 +510,36 @@ namespace osu.Game.Screens.Edit.Compose.Components
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) =>
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)
return false;
movementBlueprintOriginalPositions = null;
movementBlueprintsOriginalPositions = null;
movementBlueprints = null;
return true;

View File

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