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

Merge branch 'master' into stat_acc_anti_rake

This commit is contained in:
Givikap120 2024-10-31 15:47:52 +02:00
commit 64e694c147
38 changed files with 452 additions and 287 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1025.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new BananaHitSampleInfo(newVolume.GetOr(Volume));
public bool Equals(BananaHitSampleInfo? other)

View File

@ -1,19 +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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
@ -21,66 +10,5 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
public partial class TestSceneOsuEditor : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestTouchInputAfterTouchingComposeArea()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
AddAssert("circle placed correctly", () =>
{
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f));
});
return true;
});
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
// the final position may be slightly off from the mouse position when drawing, account for that.
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
});
return true;
});
}
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
private void tap(Vector2 position)
{
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
}
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
[TestFixture]
public partial class TestSceneSliderDrawing : TestSceneOsuEditor
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test]
public void TestTouchInputAfterTouchingComposeArea()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
AddAssert("circle placed correctly", () =>
{
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f));
});
return true;
});
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
// the final position may be slightly off from the mouse position when drawing, account for that.
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
});
return true;
});
}
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre);
private void tap(Vector2 position)
{
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
}
}
}

View File

@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
if (slider == null) return;
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70, editorAutoBank: false);
slider.Samples.Add(sample.With());
});

View File

@ -53,6 +53,8 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
AllowableAnchors = new[] { Anchor.CentreLeft, Anchor.CentreRight };
Child = new FillFlowContainer
{
Width = 220,

View File

@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Judgements
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
Ruleset.Value = new TaikoRuleset().RulesetInfo;
SelectedMods.Value = mods ?? Array.Empty<Mod>();
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });

View File

@ -7,6 +7,7 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Taiko.Edit
@ -20,6 +21,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
protected override Playfield CreatePlayfield() => new TaikoEditorPlayfield();
protected override void LoadComplete()
{
base.LoadComplete();

View File

@ -0,0 +1,25 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Edit
{
public partial class TaikoEditorPlayfield : TaikoPlayfield
{
[BackgroundDependencyLoader]
private void load()
{
// This is the simplest way to extend the taiko playfield beyond the left of the drum area.
// Required in the editor to not look weird underneath left toolbox area.
AddInternal(new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.PlayfieldBackgroundRight), _ => new PlayfieldBackgroundRight())
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopRight,
});
}
}
}

View File

@ -241,8 +241,8 @@ namespace osu.Game.Tests.Beatmaps
metadataLookup.Update(beatmapSet, preferOnlineFetch);
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
}
[Test]
@ -273,34 +273,6 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(beatmap.OnlineID, Is.EqualTo(654321));
}
[Test]
public void TestMetadataLookupForBeatmapWithoutPopulatedIDAndIncorrectHash([Values] bool preferOnlineFetch)
{
var lookupResult = new OnlineBeatmapMetadata
{
BeatmapID = 654321,
BeatmapStatus = BeatmapOnlineStatus.Ranked,
MD5Hash = @"cafebabe",
};
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
targetMock.Setup(src => src.Available).Returns(true);
targetMock.Setup(src => src.TryLookup(It.IsAny<BeatmapInfo>(), out lookupResult))
.Returns(true);
var beatmap = new BeatmapInfo
{
MD5Hash = @"deadbeef"
};
var beatmapSet = new BeatmapSetInfo(beatmap.Yield());
beatmap.BeatmapSet = beatmapSet;
metadataLookup.Update(beatmapSet, preferOnlineFetch);
Assert.That(beatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
Assert.That(beatmap.OnlineID, Is.EqualTo(-1));
}
[Test]
public void TestReturnedMetadataHasDifferentHash([Values] bool preferOnlineFetch)
{
@ -383,58 +355,5 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
}
[Test]
public void TestPartiallyMaliciousSet([Values] bool preferOnlineFetch)
{
var firstResult = new OnlineBeatmapMetadata
{
BeatmapID = 654321,
BeatmapStatus = BeatmapOnlineStatus.Ranked,
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
MD5Hash = @"cafebabe"
};
var secondResult = new OnlineBeatmapMetadata
{
BeatmapStatus = BeatmapOnlineStatus.Ranked,
BeatmapSetStatus = BeatmapOnlineStatus.Ranked,
MD5Hash = @"dededede"
};
var targetMock = preferOnlineFetch ? apiMetadataSourceMock : localCachedMetadataSourceMock;
targetMock.Setup(src => src.Available).Returns(true);
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 654321), out firstResult))
.Returns(true);
targetMock.Setup(src => src.TryLookup(It.Is<BeatmapInfo>(bi => bi.OnlineID == 666666), out secondResult))
.Returns(true);
var firstBeatmap = new BeatmapInfo
{
OnlineID = 654321,
MD5Hash = @"cafebabe",
};
var secondBeatmap = new BeatmapInfo
{
OnlineID = 666666,
MD5Hash = @"deadbeef"
};
var beatmapSet = new BeatmapSetInfo(new[]
{
firstBeatmap,
secondBeatmap
});
firstBeatmap.BeatmapSet = beatmapSet;
secondBeatmap.BeatmapSet = beatmapSet;
metadataLookup.Update(beatmapSet, preferOnlineFetch);
Assert.That(firstBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.Ranked));
Assert.That(firstBeatmap.OnlineID, Is.EqualTo(654321));
Assert.That(secondBeatmap.Status, Is.EqualTo(BeatmapOnlineStatus.None));
Assert.That(secondBeatmap.OnlineID, Is.EqualTo(-1));
Assert.That(beatmapSet.Status, Is.EqualTo(BeatmapOnlineStatus.None));
}
}
}

View File

@ -60,12 +60,18 @@ namespace osu.Game.Audio
/// </summary>
public int Volume { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100)
/// <summary>
/// Whether this sample should automatically assign the bank of the normal sample whenever it is set in the editor.
/// </summary>
public bool EditorAutoBank { get; }
public HitSampleInfo(string name, string bank = SampleControlPoint.DEFAULT_BANK, string? suffix = null, int volume = 100, bool editorAutoBank = true)
{
Name = name;
Bank = bank;
Suffix = suffix;
Volume = volume;
EditorAutoBank = editorAutoBank;
}
/// <summary>
@ -92,9 +98,10 @@ namespace osu.Game.Audio
/// <param name="newBank">An optional new sample bank.</param>
/// <param name="newSuffix">An optional new lookup suffix.</param>
/// <param name="newVolume">An optional new volume.</param>
/// <param name="newEditorAutoBank">An optional new editor auto bank flag.</param>
/// <returns>The new <see cref="HitSampleInfo"/>.</returns>
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume));
public virtual HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank));
public virtual bool Equals(HitSampleInfo? other)
=> other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix;

View File

@ -33,7 +33,7 @@ namespace osu.Game.Beatmaps
Debug.Assert(beatmapInfo.BeatmapSet != null);
var req = new GetBeatmapRequest(beatmapInfo);
var req = new GetBeatmapRequest(md5Hash: beatmapInfo.MD5Hash, filename: beatmapInfo.Path);
try
{

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Online.API;
@ -44,10 +43,19 @@ namespace osu.Game.Beatmaps
foreach (var beatmapInfo in beatmapSet.Beatmaps)
{
// note that these lookups DO NOT ACTUALLY FULLY GUARANTEE that the beatmap is what it claims it is,
// i.e. the correctness of this lookup should be treated as APPROXIMATE AT WORST.
// this is because the beatmap filename is used as a fallback in some scenarios where the MD5 of the beatmap may mismatch.
// this is considered to be an acceptable casualty so that things can continue to work as expected for users in some rare scenarios
// (stale beatmap files in beatmap packs, beatmap mirror desyncs).
// however, all this means that other places such as score submission ARE EXPECTED TO VERIFY THE MD5 OF THE BEATMAP AGAINST THE ONLINE ONE EXPLICITLY AGAIN.
//
// additionally note that the online ID stored to the map is EXPLICITLY NOT USED because some users in a silly attempt to "fix" things for themselves on stable
// would reuse online IDs of already submitted beatmaps, which means that information is pretty much expected to be bogus in a nonzero number of beatmapsets.
if (!tryLookup(beatmapInfo, preferOnlineFetch, out var res))
continue;
if (res == null || shouldDiscardLookupResult(res, beatmapInfo))
if (res == null)
{
beatmapInfo.ResetOnlineInfo();
lookupResults.Add(null); // mark lookup failure
@ -83,23 +91,6 @@ namespace osu.Game.Beatmaps
}
}
private bool shouldDiscardLookupResult(OnlineBeatmapMetadata result, BeatmapInfo beatmapInfo)
{
if (beatmapInfo.OnlineID > 0 && result.BeatmapID != beatmapInfo.OnlineID)
{
Logger.Log($"Discarding metadata lookup result due to mismatching online ID (expected: {beatmapInfo.OnlineID} actual: {result.BeatmapID})", LoggingTarget.Database);
return true;
}
if (beatmapInfo.OnlineID == -1 && result.MD5Hash != beatmapInfo.MD5Hash)
{
Logger.Log($"Discarding metadata lookup result due to mismatching hash (expected: {beatmapInfo.MD5Hash} actual: {result.MD5Hash})", LoggingTarget.Database);
return true;
}
return false;
}
/// <summary>
/// Attempts to retrieve the <see cref="OnlineBeatmapMetadata"/> for the given <paramref name="beatmapInfo"/>.
/// </summary>

View File

@ -539,7 +539,7 @@ namespace osu.Game.Beatmaps.Formats
private string getSampleBank(IList<HitSampleInfo> samples, bool banksOnly = false)
{
LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank);
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank);
LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL && !s.EditorAutoBank)?.Bank);
StringBuilder sb = new StringBuilder();

View File

@ -90,8 +90,7 @@ namespace osu.Game.Beatmaps
}
if (string.IsNullOrEmpty(beatmapInfo.MD5Hash)
&& string.IsNullOrEmpty(beatmapInfo.Path)
&& beatmapInfo.OnlineID <= 0)
&& string.IsNullOrEmpty(beatmapInfo.Path))
{
onlineMetadata = null;
return false;
@ -240,10 +239,9 @@ namespace osu.Game.Beatmaps
using var cmd = db.CreateCommand();
cmd.CommandText =
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineID OR filename = @Path";
@"SELECT beatmapset_id, beatmap_id, approved, user_id, checksum, last_update FROM osu_beatmaps WHERE checksum = @MD5Hash OR filename = @Path";
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
using var reader = cmd.ExecuteReader();
@ -281,11 +279,10 @@ namespace osu.Game.Beatmaps
SELECT `b`.`beatmapset_id`, `b`.`beatmap_id`, `b`.`approved`, `b`.`user_id`, `b`.`checksum`, `b`.`last_update`, `s`.`submit_date`, `s`.`approved_date`
FROM `osu_beatmaps` AS `b`
JOIN `osu_beatmapsets` AS `s` ON `s`.`beatmapset_id` = `b`.`beatmapset_id`
WHERE `b`.`checksum` = @MD5Hash OR `b`.`beatmap_id` = @OnlineID OR `b`.`filename` = @Path
WHERE `b`.`checksum` = @MD5Hash OR `b`.`filename` = @Path
""";
cmd.Parameters.Add(new SqliteParameter(@"@MD5Hash", beatmapInfo.MD5Hash));
cmd.Parameters.Add(new SqliteParameter(@"@OnlineID", beatmapInfo.OnlineID));
cmd.Parameters.Add(new SqliteParameter(@"@Path", beatmapInfo.Path));
using var reader = cmd.ExecuteReader();

View File

@ -206,6 +206,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorTimelineShowTimingChanges, true);
SetDefault(OsuSetting.EditorTimelineShowTicks, true);
SetDefault(OsuSetting.EditorContractSidebars, false);
SetDefault(OsuSetting.AlwaysShowHoldForMenuButton, false);
}
@ -431,6 +433,7 @@ namespace osu.Game.Configuration
HideCountryFlags,
EditorTimelineShowTimingChanges,
EditorTimelineShowTicks,
AlwaysShowHoldForMenuButton
AlwaysShowHoldForMenuButton,
EditorContractSidebars
}
}

View File

@ -528,7 +528,7 @@ namespace osu.Game.Database
/// <param name="model">The new model proposed for import.</param>
/// <param name="realm">The current realm context.</param>
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().FirstOrDefault(b => b.Hash == model.Hash);
protected TModel? CheckForExisting(TModel model, Realm realm) => string.IsNullOrEmpty(model.Hash) ? null : realm.All<TModel>().OrderBy(b => b.DeletePending).FirstOrDefault(b => b.Hash == model.Hash);
/// <summary>
/// Whether import can be skipped after finding an existing import early in the process.

View File

@ -98,7 +98,7 @@ namespace osu.Game.Graphics.UserInterface
{
const float vertical_offset = 13;
InternalChildren = new Drawable[]
InternalChildren = new[]
{
label = new OsuSpriteText
{
@ -115,7 +115,9 @@ namespace osu.Game.Graphics.UserInterface
KeyboardStep = 0.1f,
RelativeSizeAxes = Axes.X,
Y = vertical_offset,
}
},
upperBound.Nub.CreateProxy(),
lowerBound.Nub.CreateProxy(),
};
}
@ -160,6 +162,8 @@ namespace osu.Game.Graphics.UserInterface
protected partial class BoundSlider : RoundedSliderBar<double>
{
public new Nub Nub => base.Nub;
public string? DefaultString;
public LocalisableString? DefaultTooltip;
public string? TooltipSuffix;

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
@ -25,6 +26,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly HoverClickSounds hoverClickSounds;
private readonly Container mainContent;
private Color4 accentColour;
public Color4 AccentColour
@ -62,7 +65,7 @@ namespace osu.Game.Graphics.UserInterface
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new CircularContainer
Child = mainContent = new CircularContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -135,6 +138,26 @@ namespace osu.Game.Graphics.UserInterface
}, true);
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
mainContent.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = AccentColour.Darken(1),
Hollow = true,
Radius = 2,
};
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
mainContent.EdgeEffect = default;
}
protected override bool OnHover(HoverEvent e)
{
updateGlow();

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
@ -26,6 +27,8 @@ namespace osu.Game.Graphics.UserInterface
private readonly HoverClickSounds hoverClickSounds;
private readonly Container mainContent;
private Color4 accentColour;
public Color4 AccentColour
@ -60,12 +63,13 @@ namespace osu.Game.Graphics.UserInterface
RangePadding = EXPANDED_SIZE / 2;
Children = new Drawable[]
{
new Container
mainContent = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@ -138,6 +142,26 @@ namespace osu.Game.Graphics.UserInterface
}, true);
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
mainContent.EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Colour = AccentColour.Darken(1),
Hollow = true,
Radius = 2,
};
}
protected override void OnFocusLost(FocusLostEvent e)
{
base.OnFocusLost(e);
mainContent.EdgeEffect = default;
}
protected override bool OnHover(HoverEvent e)
{
updateGlow();
@ -167,8 +191,8 @@ namespace osu.Game.Graphics.UserInterface
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.3f, 0, Math.Max(0, DrawWidth)), 1);
}
protected override void UpdateValue(float value)

View File

@ -71,7 +71,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private Box background = null!;
private Box flashLayer = null!;
private FormTextBox.InnerTextBox textBox = null!;
private Slider slider = null!;
private InnerSlider slider = null!;
private FormFieldCaption caption = null!;
private IFocusManager focusManager = null!;
@ -135,7 +135,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
},
TabbableContentContainer = tabbableContentContainer,
},
slider = new Slider
slider = new InnerSlider
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
@ -163,6 +163,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox.Current.BindValueChanged(textChanged);
slider.IsDragging.BindValueChanged(_ => updateState());
slider.Focused.BindValueChanged(_ => updateState());
current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue;
current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
@ -259,16 +260,18 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState()
{
bool childHasFocus = slider.Focused.Value || textBox.Focused.Value;
textBox.Alpha = 1;
background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0;
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
BorderThickness = childHasFocus || IsHovered || slider.IsDragging.Value ? 2 : 0;
BorderColour = childHasFocus ? colourProvider.Highlight1 : colourProvider.Light4;
if (textBox.Focused.Value)
if (childHasFocus)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark3);
else if (IsHovered || slider.IsDragging.Value)
background.Colour = ColourInfo.GradientVertical(colourProvider.Background5, colourProvider.Dark4);
@ -283,8 +286,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
textBox.Text = slider.GetDisplayableValue(currentNumberInstantaneous.Value).ToString();
}
private partial class Slider : OsuSliderBar<T>
private partial class InnerSlider : OsuSliderBar<T>
{
public BindableBool Focused { get; } = new BindableBool();
public BindableBool IsDragging { get; set; } = new BindableBool();
public Action? OnCommit { get; set; }
@ -344,7 +349,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
@ -382,11 +386,25 @@ namespace osu.Game.Graphics.UserInterfaceV2
base.OnHoverLost(e);
}
protected override void OnFocus(FocusEvent e)
{
updateState();
Focused.Value = true;
base.OnFocus(e);
}
protected override void OnFocusLost(FocusLostEvent e)
{
updateState();
Focused.Value = false;
base.OnFocusLost(e);
}
private void updateState()
{
rightBox.Colour = colourProvider.Background6;
leftBox.Colour = IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
nub.Colour = IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
leftBox.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1.Opacity(0.5f) : colourProvider.Dark2;
nub.Colour = HasFocus || IsHovered || IsDragged ? colourProvider.Highlight1 : colourProvider.Light4;
}
protected override void UpdateValue(float value)

View File

@ -114,6 +114,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
/// <summary>
/// "Contract sidebars when not hovered"
/// </summary>
public static LocalisableString ContractSidebars => new TranslatableString(getKey(@"contract_sidebars"), @"Contract sidebars when not hovered");
/// <summary>
/// "Must be in edit mode to handle editor links"
/// </summary>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Globalization;
using osu.Framework.IO.Network;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
@ -9,23 +10,30 @@ namespace osu.Game.Online.API.Requests
{
public class GetBeatmapRequest : APIRequest<APIBeatmap>
{
public readonly IBeatmapInfo BeatmapInfo;
public readonly string Filename;
public readonly int OnlineID;
public readonly string? MD5Hash;
public readonly string? Filename;
public GetBeatmapRequest(IBeatmapInfo beatmapInfo)
: this(onlineId: beatmapInfo.OnlineID, md5Hash: beatmapInfo.MD5Hash, filename: (beatmapInfo as BeatmapInfo)?.Path)
{
BeatmapInfo = beatmapInfo;
Filename = (beatmapInfo as BeatmapInfo)?.Path ?? string.Empty;
}
public GetBeatmapRequest(int onlineId = -1, string? md5Hash = null, string? filename = null)
{
OnlineID = onlineId;
MD5Hash = md5Hash;
Filename = filename;
}
protected override WebRequest CreateWebRequest()
{
var request = base.CreateWebRequest();
if (BeatmapInfo.OnlineID > 0)
request.AddParameter(@"id", BeatmapInfo.OnlineID.ToString());
if (!string.IsNullOrEmpty(BeatmapInfo.MD5Hash))
request.AddParameter(@"checksum", BeatmapInfo.MD5Hash);
if (OnlineID > 0)
request.AddParameter(@"id", OnlineID.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrEmpty(MD5Hash))
request.AddParameter(@"checksum", MD5Hash);
if (!string.IsNullOrEmpty(Filename))
request.AddParameter(@"filename", Filename);

View File

@ -1,9 +1,13 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Screens.Edit;
using osuTK;
namespace osu.Game.Rulesets.Edit
@ -12,6 +16,15 @@ namespace osu.Game.Rulesets.Edit
{
protected override double HoverExpansionDelay => 250;
protected override bool ExpandOnHover => expandOnHover;
private readonly Bindable<bool> contractSidebars = new Bindable<bool>();
private bool expandOnHover;
[Resolved]
private Editor? editor { get; set; }
public ExpandingToolboxContainer(float contractedWidth, float expandedWidth)
: base(contractedWidth, expandedWidth)
{
@ -19,6 +32,27 @@ namespace osu.Game.Rulesets.Edit
FillFlow.Spacing = new Vector2(5);
FillFlow.Padding = new MarginPadding { Vertical = 5 };
Expanded.Value = true;
}
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
config.BindWith(OsuSetting.EditorContractSidebars, contractSidebars);
}
protected override void Update()
{
base.Update();
bool requireContracting = contractSidebars.Value || editor?.DrawSize.X / editor?.DrawSize.Y < 1.7f;
if (expandOnHover != requireContracting)
{
expandOnHover = requireContracting;
Expanded.Value = !expandOnHover;
}
}
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => base.ReceivePositionalInputAtSubTree(screenSpacePos) && anyToolboxHovered(screenSpacePos);

View File

@ -344,8 +344,8 @@ namespace osu.Game.Rulesets.Edit
PlayfieldContentContainer.Anchor = Anchor.CentreLeft;
PlayfieldContentContainer.Origin = Anchor.CentreLeft;
PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth) - (TOOLBOX_CONTRACTED_SIZE_LEFT + TOOLBOX_CONTRACTED_SIZE_RIGHT);
PlayfieldContentContainer.X = TOOLBOX_CONTRACTED_SIZE_LEFT;
PlayfieldContentContainer.Width = Math.Max(1024, DrawWidth);
PlayfieldContentContainer.X = LeftToolbox.DrawWidth;
}
composerFocusMode.Value = PlayfieldContentContainer.Contains(InputManager.CurrentState.Mouse.Position)

View File

@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Objects
// Fall back to using the normal sample bank otherwise.
if (Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL) is HitSampleInfo existingNormal)
return existingNormal.With(newName: sampleName);
return existingNormal.With(newName: sampleName, newEditorAutoBank: true);
return new HitSampleInfo(sampleName);
}

View File

@ -204,8 +204,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (stringBank == @"none")
stringBank = null;
string stringAddBank = addBank.ToString().ToLowerInvariant();
if (stringAddBank == @"none")
{
bankInfo.EditorAutoBank = true;
stringAddBank = null;
}
else
bankInfo.EditorAutoBank = false;
bankInfo.BankForNormal = stringBank;
bankInfo.BankForAdditions = string.IsNullOrEmpty(stringAddBank) ? stringBank : stringAddBank;
@ -477,7 +483,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (string.IsNullOrEmpty(bankInfo.Filename))
{
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, true, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)));
@ -489,13 +495,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
if (type.HasFlag(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank));
if (type.HasFlag(LegacyHitSoundType.Whistle))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank));
if (type.HasFlag(LegacyHitSoundType.Clap))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.EditorAutoBank, bankInfo.CustomSampleBank));
return soundTypes;
}
@ -534,6 +540,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary>
public int CustomSampleBank;
/// <summary>
/// Whether the bank for additions should be inherited from the normal sample in edit.
/// </summary>
public bool EditorAutoBank = true;
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
}
@ -558,21 +569,21 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary>
public bool BankSpecified;
public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false)
: base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume)
public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, bool editorAutoBank = false, int customSampleBank = 0, bool isLayered = false)
: base(name, bank ?? SampleControlPoint.DEFAULT_BANK, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume, editorAutoBank)
{
CustomSampleBank = customSampleBank;
BankSpecified = !string.IsNullOrEmpty(bank);
IsLayered = isLayered;
}
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default)
=> With(newName, newBank, newVolume);
public sealed override HitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<string?> newSuffix = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default)
=> With(newName, newBank, newVolume, newEditorAutoBank);
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
public virtual LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
=> new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newEditorAutoBank.GetOr(EditorAutoBank), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered));
public bool Equals(LegacyHitSampleInfo? other)
// The additions to equality checks here are *required* to ensure that pooling works correctly.
@ -604,7 +615,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
Path.ChangeExtension(Filename, null)
};
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default,
public sealed override LegacyHitSampleInfo With(Optional<string> newName = default, Optional<string> newBank = default, Optional<int> newVolume = default, Optional<bool> newEditorAutoBank = default,
Optional<int> newCustomSampleBank = default,
Optional<bool> newIsLayered = default)
=> new FileHitSampleInfo(Filename, newVolume.GetOr(Volume));

View File

@ -101,7 +101,7 @@ namespace osu.Game.Rulesets
/// <param name="acronym">The acronym to query for .</param>
public Mod? CreateModFromAcronym(string acronym)
{
return AllMods.FirstOrDefault(m => m.Acronym == acronym)?.CreateInstance();
return AllMods.FirstOrDefault(m => string.Equals(m.Acronym, acronym, StringComparison.OrdinalIgnoreCase))?.CreateInstance();
}
/// <summary>

View File

@ -4,8 +4,10 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
@ -14,7 +16,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public partial class DrawableTernaryButton : OsuButton
public partial class DrawableTernaryButton : OsuButton, IHasTooltip
{
private Color4 defaultBackgroundColour;
private Color4 defaultIconColour;
@ -58,12 +60,16 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
base.LoadComplete();
Button.Bindable.BindValueChanged(_ => updateSelectionState(), true);
Button.Enabled.BindTo(Enabled);
Action = onAction;
}
private void onAction()
{
if (!Button.Enabled.Value)
return;
Button.Toggle();
}
@ -98,5 +104,7 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
Anchor = Anchor.CentreLeft,
X = 40f
};
public LocalisableString TooltipText => Button.Tooltip;
}
}

View File

@ -12,6 +12,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
{
public readonly Bindable<TernaryState> Bindable;
public readonly Bindable<bool> Enabled = new Bindable<bool>(true);
public readonly string Description;
/// <summary>
@ -19,6 +21,8 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
/// </summary>
public readonly Func<Drawable>? CreateIcon;
public string Tooltip { get; set; } = string.Empty;
public TernaryButton(Bindable<TernaryState> bindable, string description, Func<Drawable>? createIcon = null)
{
Bindable = bindable;

View File

@ -68,6 +68,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
SampleBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionBankStates).ToArray();
SampleAdditionBankTernaryStates = createSampleBankTernaryButtons(SelectionHandler.SelectionAdditionBankStates).ToArray();
SelectionHandler.AutoSelectionBankEnabled.BindValueChanged(_ => updateAutoBankTernaryButtonTooltip(), true);
SelectionHandler.SelectionAdditionBanksEnabled.BindValueChanged(_ => updateAdditionBankTernaryButtonTooltips(), true);
AddInternal(new DrawableRulesetDependenciesProvidingContainer(Composer.Ruleset)
{
Child = placementBlueprintContainer
@ -288,6 +291,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
return null;
}
private void updateAutoBankTernaryButtonTooltip()
{
bool enabled = SelectionHandler.AutoSelectionBankEnabled.Value;
var autoBankButton = SampleBankTernaryStates.Single(t => t.Bindable == SelectionHandler.SelectionBankStates[EditorSelectionHandler.HIT_BANK_AUTO]);
autoBankButton.Enabled.Value = enabled;
autoBankButton.Tooltip = !enabled ? "Auto normal bank can only be used during hit object placement" : string.Empty;
}
private void updateAdditionBankTernaryButtonTooltips()
{
bool enabled = SelectionHandler.SelectionAdditionBanksEnabled.Value;
foreach (var ternaryButton in SampleAdditionBankTernaryStates)
{
ternaryButton.Enabled.Value = enabled;
ternaryButton.Tooltip = !enabled ? "Add an addition sample first to be able to set a bank" : string.Empty;
}
}
#region Placement
/// <summary>

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
@ -10,7 +11,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// bring in updates from selection changes
EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates);
SelectedItems.CollectionChanged += (_, _) => Scheduler.AddOnce(UpdateTernaryStates);
SelectedItems.CollectionChanged += onSelectedItemsChanged;
}
protected override void DeleteItems(IEnumerable<HitObject> items) => EditorBeatmap.RemoveRange(items);
@ -64,6 +64,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
public readonly Dictionary<string, Bindable<TernaryState>> SelectionAdditionBankStates = new Dictionary<string, Bindable<TernaryState>>();
/// <summary>
/// Whether there is no selection and the auto <see cref="SelectionBankStates"/> can be used.
/// </summary>
public readonly Bindable<bool> AutoSelectionBankEnabled = new Bindable<bool>();
/// <summary>
/// Whether the selection contains any addition samples and the <see cref="SelectionAdditionBankStates"/> can be used.
/// </summary>
public readonly Bindable<bool> SelectionAdditionBanksEnabled = new Bindable<bool>();
/// <summary>
/// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions)
/// </summary>
@ -153,10 +163,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
else
{
// Auto should never apply when there is a selection made.
if (bankName == HIT_BANK_AUTO)
break;
// Completely empty selections should be allowed in the case that none of the selected objects have any addition samples.
// This is also required to stop a bindable feedback loop when a HitObject has zero addition samples (and LINQ `All` below becomes true).
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
@ -164,8 +170,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Never remove a sample bank.
// These are basically radio buttons, not toggles.
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName)))
bindable.Value = TernaryState.True;
if (bankName == HIT_BANK_AUTO)
{
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.EditorAutoBank)))
bindable.Value = TernaryState.True;
}
else
{
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName && !s.EditorAutoBank)))
bindable.Value = TernaryState.True;
}
}
break;
@ -183,14 +197,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
}
else
{
// Auto should just not apply if there's a selection already made.
// Maybe we could make it a disabled button in the future, but right now the editor buttons don't support disabled state.
if (bankName == HIT_BANK_AUTO)
{
bindable.Value = TernaryState.False;
break;
}
// If none of the selected objects have any addition samples, we should not apply the addition bank.
if (SelectedItems.SelectMany(enumerateAllSamples).All(h => h.All(o => o.Name == HitSampleInfo.HIT_NORMAL)))
{
@ -208,9 +214,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionAdditionBankStates[bankName] = bindable;
}
// start with normal selected.
SelectionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
SelectionAdditionBankStates[SampleControlPoint.DEFAULT_BANK].Value = TernaryState.True;
resetTernaryStates();
foreach (string sampleName in HitSampleInfo.AllAdditions)
{
@ -252,12 +256,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
};
}
private void resetTernaryStates()
{
AutoSelectionBankEnabled.Value = true;
SelectionAdditionBanksEnabled.Value = true;
SelectionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
SelectionAdditionBankStates[HIT_BANK_AUTO].Value = TernaryState.True;
}
/// <summary>
/// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated).
/// </summary>
protected virtual void UpdateTernaryStates()
{
SelectionNewComboState.Value = GetStateFromSelection(SelectedItems.OfType<IHasComboInformation>(), h => h.NewCombo);
AutoSelectionBankEnabled.Value = SelectedItems.Count == 0;
var samplesInSelection = SelectedItems.SelectMany(enumerateAllSamples).ToArray();
@ -271,12 +284,23 @@ namespace osu.Game.Screens.Edit.Compose.Components
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name == HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
}
SelectionAdditionBanksEnabled.Value = samplesInSelection.SelectMany(s => s).Any(o => o.Name != HitSampleInfo.HIT_NORMAL);
foreach ((string bankName, var bindable) in SelectionAdditionBankStates)
{
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => h.Bank == bankName);
bindable.Value = GetStateFromSelection(samplesInSelection.SelectMany(s => s).Where(o => o.Name != HitSampleInfo.HIT_NORMAL), h => (bankName != HIT_BANK_AUTO && h.Bank == bankName && !h.EditorAutoBank) || (bankName == HIT_BANK_AUTO && h.EditorAutoBank));
}
}
private void onSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
// Reset the ternary states when the selection is cleared.
if (e.OldStartingIndex >= 0 && e.NewStartingIndex < 0)
Scheduler.AddOnce(resetTernaryStates);
else
Scheduler.AddOnce(UpdateTernaryStates);
}
private IEnumerable<IList<HitSampleInfo>> enumerateAllSamples(HitObject hitObject)
{
yield return hitObject.Samples;
@ -337,33 +361,29 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="bankName">The name of the sample bank.</param>
public void SetSampleAdditionBank(string bankName)
{
bool hasRelevantBank(HitObject hitObject)
{
bool result = hitObject.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
if (hitObject is IHasRepeats hasRepeats)
{
foreach (var node in hasRepeats.NodeSamples)
result &= node.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName);
}
return result;
}
bool hasRelevantBank(HitObject hitObject) =>
bankName == HIT_BANK_AUTO
? enumerateAllSamples(hitObject).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.EditorAutoBank)
: enumerateAllSamples(hitObject).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName && !s.EditorAutoBank);
if (SelectedItems.All(hasRelevantBank))
return;
EditorBeatmap.PerformOnSelection(h =>
{
if (enumerateAllSamples(h).SelectMany(o => o).Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(s => s.Bank == bankName))
if (hasRelevantBank(h))
return;
h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
string normalBank = h.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
h.Samples = h.Samples.Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList();
if (h is IHasRepeats hasRepeats)
{
for (int i = 0; i < hasRepeats.NodeSamples.Count; ++i)
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? s.With(newBank: bankName) : s).ToList();
{
normalBank = hasRepeats.NodeSamples[i].FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
hasRepeats.NodeSamples[i] = hasRepeats.NodeSamples[i].Select(s => s.Name != HitSampleInfo.HIT_NORMAL ? bankName == HIT_BANK_AUTO ? s.With(newBank: normalBank, newEditorAutoBank: true) : s.With(newBank: bankName, newEditorAutoBank: false) : s).ToList();
}
}
EditorBeatmap.Update(h);
@ -407,9 +427,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
var hitSample = h.CreateHitSampleInfo(sampleName);
string? existingAdditionBank = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL)?.Bank;
if (existingAdditionBank != null)
hitSample = hitSample.With(newBank: existingAdditionBank);
HitSampleInfo? existingAddition = node.FirstOrDefault(s => s.Name != HitSampleInfo.HIT_NORMAL);
if (existingAddition != null)
hitSample = hitSample.With(newBank: existingAddition.Bank, newEditorAutoBank: existingAddition.EditorAutoBank);
node.Add(hitSample);
}

View File

@ -107,7 +107,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
public static string? GetAdditionBankValue(IEnumerable<HitSampleInfo> samples)
{
return samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL)?.Bank;
var firstAddition = samples.FirstOrDefault(o => o.Name != HitSampleInfo.HIT_NORMAL);
if (firstAddition == null)
return null;
return firstAddition.EditorAutoBank ? EditorSelectionHandler.HIT_BANK_AUTO : firstAddition.Bank;
}
public static int GetVolumeValue(ICollection<HitSampleInfo> samples)
@ -320,7 +324,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
for (int i = 0; i < relevantSamples.Count; i++)
{
if (relevantSamples[i].Name != HitSampleInfo.HIT_NORMAL) continue;
if (relevantSamples[i].Name != HitSampleInfo.HIT_NORMAL && !relevantSamples[i].EditorAutoBank) continue;
relevantSamples[i] = relevantSamples[i].With(newBank: newBank);
}
@ -331,11 +335,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
updateAllRelevantSamples((_, relevantSamples) =>
{
string normalBank = relevantSamples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank ?? HitSampleInfo.BANK_SOFT;
for (int i = 0; i < relevantSamples.Count; i++)
{
if (relevantSamples[i].Name == HitSampleInfo.HIT_NORMAL) continue;
if (relevantSamples[i].Name == HitSampleInfo.HIT_NORMAL)
continue;
relevantSamples[i] = relevantSamples[i].With(newBank: newBank);
// Addition samples with bank set to auto should inherit the bank of the normal sample
if (newBank == EditorSelectionHandler.HIT_BANK_AUTO)
{
relevantSamples[i] = relevantSamples[i].With(newBank: normalBank, newEditorAutoBank: true);
}
else
relevantSamples[i] = relevantSamples[i].With(newBank: newBank, newEditorAutoBank: false);
}
});
}
@ -383,7 +396,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
selectionSampleStates[sampleName] = bindable;
}
banks.AddRange(HitSampleInfo.AllBanks);
banks.AddRange(HitSampleInfo.AllBanks.Prepend(EditorSelectionHandler.HIT_BANK_AUTO));
}
private void updateTernaryStates()
@ -448,7 +461,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (string.IsNullOrEmpty(newBank))
return true;
if (e.ShiftPressed)
if (e.ShiftPressed && newBank != EditorSelectionHandler.HIT_BANK_AUTO)
{
setBank(newBank);
updatePrimaryBankState();
@ -462,7 +475,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
else
{
var item = togglesCollection.ElementAtOrDefault(rightIndex);
var item = togglesCollection.ElementAtOrDefault(rightIndex - 1);
if (item is not DrawableTernaryButton button) return base.OnKeyDown(e);
@ -476,18 +489,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
switch (key)
{
case Key.W:
case Key.Q:
index = 0;
break;
case Key.E:
case Key.W:
index = 1;
break;
case Key.R:
case Key.E:
index = 2;
break;
case Key.R:
index = 3;
break;
default:
index = -1;
break;

View File

@ -215,6 +215,7 @@ namespace osu.Game.Screens.Edit
private Bindable<bool> editorLimitedDistanceSnap;
private Bindable<bool> editorTimelineShowTimingChanges;
private Bindable<bool> editorTimelineShowTicks;
private Bindable<bool> editorContractSidebars;
/// <summary>
/// This controls the opacity of components like the timelines, sidebars, etc.
@ -323,6 +324,7 @@ namespace osu.Game.Screens.Edit
editorLimitedDistanceSnap = config.GetBindable<bool>(OsuSetting.EditorLimitedDistanceSnap);
editorTimelineShowTimingChanges = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTimingChanges);
editorTimelineShowTicks = config.GetBindable<bool>(OsuSetting.EditorTimelineShowTicks);
editorContractSidebars = config.GetBindable<bool>(OsuSetting.EditorContractSidebars);
AddInternal(new OsuContextMenuContainer
{
@ -402,7 +404,11 @@ namespace osu.Game.Screens.Edit
new ToggleMenuItem(EditorStrings.LimitedDistanceSnap)
{
State = { BindTarget = editorLimitedDistanceSnap },
}
},
new ToggleMenuItem(EditorStrings.ContractSidebars)
{
State = { BindTarget = editorContractSidebars }
},
}
},
new MenuItem(EditorStrings.Timing)

View File

@ -562,11 +562,8 @@ namespace osu.Game.Screens.Play
}
catch (BeatmapInvalidForRulesetException)
{
// A playable beatmap may not be creatable with the user's preferred ruleset, so try using the beatmap's default ruleset
rulesetInfo = Beatmap.Value.BeatmapInfo.Ruleset;
ruleset = rulesetInfo.CreateInstance();
playable = Beatmap.Value.GetPlayableBeatmap(rulesetInfo, gameplayMods, cancellationToken);
Logger.Log($"The current beatmap is not playable in {ruleset.RulesetInfo.Name}!", level: LogLevel.Important);
return null;
}
if (playable.HitObjects.Count == 0)

View File

@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
case GetBeatmapRequest getBeatmapRequest:
{
getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.BeatmapInfo.OnlineID).Single());
getBeatmapRequest.TriggerSuccess(createResponseBeatmaps(getBeatmapRequest.OnlineID).Single());
return true;
}

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1009.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1025.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1003.0" />
<PackageReference Include="Sentry" Version="4.12.1" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -17,6 +17,6 @@
<MtouchInterpreter>-all</MtouchInterpreter>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1009.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.1025.0" />
</ItemGroup>
</Project>