1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 09:32:56 +08:00

Merge branch 'master' into catch-editor-per-object-sv

This commit is contained in:
Dean Herbert 2022-05-09 19:18:01 +09:00 committed by GitHub
commit c7e9bd7751
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 540 additions and 102 deletions

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.509.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -55,7 +55,10 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0);
AddAssert("duration is positive", () => ((BananaShower)CurrentBlueprint.HitObject).Duration > 0);
AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));

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;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
@ -13,11 +14,21 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{
private readonly TimeSpanOutline outline;
private double placementStartTime;
private double placementEndTime;
public BananaShowerPlacementBlueprint()
{
InternalChild = outline = new TimeSpanOutline();
}
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update()
{
base.Update();
@ -38,13 +49,6 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
case PlacementState.Active:
if (e.Button != MouseButton.Right) break;
// If the duration is negative, swap the start and the end time to make the duration positive.
if (HitObject.Duration < 0)
{
HitObject.StartTime = HitObject.EndTime;
HitObject.Duration = -HitObject.Duration;
}
EndPlacement(HitObject.Duration > 0);
return true;
}
@ -61,13 +65,16 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
switch (PlacementActive)
{
case PlacementState.Waiting:
HitObject.StartTime = time;
placementStartTime = placementEndTime = time;
break;
case PlacementState.Active:
HitObject.EndTime = time;
placementEndTime = time;
break;
}
HitObject.StartTime = Math.Min(placementStartTime, placementEndTime);
HitObject.EndTime = Math.Max(placementStartTime, placementEndTime);
}
}
}

View File

@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
base.LoadComplete();
inputManager = GetContainingInputManager();
BeginPlacement();
}
protected override bool OnMouseDown(MouseDownEvent e)

View File

@ -107,6 +107,18 @@ namespace osu.Game.Rulesets.Osu.Mods
{
switch (e.Type)
{
case SliderEventType.Tick:
AddNested(new SliderTick
{
SpanIndex = e.SpanIndex,
SpanStartTime = e.SpanStartTime,
StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
Scale = Scale,
});
break;
case SliderEventType.Head:
AddNested(HeadCircle = new SliderHeadCircle
{

View File

@ -137,33 +137,137 @@ namespace osu.Game.Tests.Mods
// incompatible pair.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() },
new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
},
// incompatible pair with derived class.
new object[]
{
new Mod[] { new OsuModNightcore(), new OsuModHalfTime() },
new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() },
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModDaycore() },
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) }
},
// invalid multiplayer mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
null
},
// invalid free mod is valid for local.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null
},
// valid pair.
new object[]
{
new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() },
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null
}
},
};
private static readonly object[] invalid_multiplayer_mod_test_scenarios =
{
// incompatible pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
new[] { typeof(OsuModHidden), typeof(OsuModApproachDifferent) }
},
// incompatible pair with derived class.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) }
},
// invalid multiplayer mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
new[] { typeof(InvalidMultiplayerMod) }
},
// invalid free mod is valid for multiplayer global.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
null
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null
},
};
private static readonly object[] invalid_free_mod_test_scenarios =
{
// system mod.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
},
// multi mod.
new object[]
{
new Mod[] { new MultiMod(new OsuModSuddenDeath(), new OsuModPerfect()) },
new[] { typeof(MultiMod) }
},
// invalid multiplayer mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerMod() },
new[] { typeof(InvalidMultiplayerMod) }
},
// invalid free mod.
new object[]
{
new Mod[] { new OsuModHidden(), new InvalidMultiplayerFreeMod() },
new[] { typeof(InvalidMultiplayerFreeMod) }
},
// incompatible pair is valid for free mods.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModApproachDifferent() },
null,
},
// incompatible pair with derived class is valid for free mods.
new object[]
{
new Mod[] { new OsuModDeflate(), new OsuModSpinIn() },
null,
},
// valid pair.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModHardRock() },
null
},
};
[TestCaseSource(nameof(invalid_mod_test_scenarios))]
@ -179,6 +283,32 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_multiplayer_mod_test_scenarios))]
public void TestInvalidMultiplayerModScenarios(Mod[] inputMods, Type[] expectedInvalid)
{
bool isValid = ModUtils.CheckValidRequiredModsForMultiplayer(inputMods, out var invalid);
Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
if (isValid)
Assert.IsNull(invalid);
else
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[TestCaseSource(nameof(invalid_free_mod_test_scenarios))]
public void TestInvalidFreeModScenarios(Mod[] inputMods, Type[] expectedInvalid)
{
bool isValid = ModUtils.CheckValidFreeModsForMultiplayer(inputMods, out var invalid);
Assert.That(isValid, Is.EqualTo(expectedInvalid == null));
if (isValid)
Assert.IsNull(invalid);
else
Assert.That(invalid.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
public abstract class CustomMod1 : Mod, IModCompatibilitySpecification
{
}
@ -187,6 +317,27 @@ namespace osu.Game.Tests.Mods
{
}
public class InvalidMultiplayerMod : Mod
{
public override string Name => string.Empty;
public override string Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
}
private class InvalidMultiplayerFreeMod : Mod
{
public override string Name => string.Empty;
public override string Description => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override bool HasImplementation => true;
public override bool ValidForMultiplayerAsFreeMod => false;
}
public interface IModCompatibilitySpecification
{
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects.Drawables;

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures;
using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual.Gameplay
@ -131,7 +130,6 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Cached(typeof(ISkinSource))]
[Cached(typeof(ISamplePlaybackDisabler))]
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
{
[Resolved]

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -176,5 +177,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
.ChildrenOfType<ModPanel>()
.SingleOrDefault(panel => !panel.Filtered.Value)?.Mod is OsuModDoubleTime);
}
[Test]
public void TestNextPlaylistItemSelectedAfterCompletion()
{
AddStep("add two playlist items", () =>
{
SelectedRoom.Value.Playlist.AddRange(new[]
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
},
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
});
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
ClickButtonWhenEnabled<MultiplayerReadyButton>();
ClickButtonWhenEnabled<MultiplayerReadyButton>();
AddStep("change user to loaded", () => MultiplayerClient.ChangeState(MultiplayerUserState.Loaded));
AddUntilStep("user playing", () => MultiplayerClient.LocalUser?.State == MultiplayerUserState.Playing);
AddStep("abort gameplay", () => MultiplayerClient.AbortGameplay());
AddUntilStep("last playlist item selected", () =>
{
var lastItem = this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(p => p.Item.ID == MultiplayerClient.APIRoom?.Playlist.Last().ID);
return lastItem.IsSelectedItem;
});
}
}
}

View File

@ -8,11 +8,23 @@ using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerTeamResults : ScreenTestScene
{
[Test]
public void TestScaling()
{
// scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason.
AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() =>
{
Stack.Scale = new Vector2(v);
Stack.Size = new Vector2(1f / v);
}));
}
[TestCase(7483253, 1048576)]
[TestCase(1048576, 7483253)]
[TestCase(1048576, 1048576)]

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
@ -503,6 +504,22 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("test dispose doesn't crash", () => Game.Dispose());
}
[Test]
public void TestRapidBackButtonExit()
{
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
AddStep("press escape twice rapidly", () =>
{
InputManager.Key(Key.Escape);
InputManager.Key(Key.Escape);
});
pushEscape();
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog != null);
}
private Func<Player> playToResults()
{
Player player = null;

View File

@ -56,6 +56,17 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestScaling()
{
// scheduling is needed as scaling the content immediately causes the entire scene to shake badly, for some odd reason.
AddSliderStep("scale", 0.5f, 1.6f, 1f, v => Schedule(() =>
{
Content.Scale = new Vector2(v);
Content.Size = new Vector2(1f / v);
}));
}
[Test]
public void TestResultsWithoutPlayer()
{

View File

@ -451,6 +451,36 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod select hidden", () => modSelectScreen.State.Value == Visibility.Hidden);
}
[Test]
public void TestColumnHiding()
{
AddStep("create screen", () => Child = modSelectScreen = new UserModSelectScreen
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = mod => mod.Type == ModType.DifficultyIncrease || mod.Type == ModType.Conversion
});
waitForColumnLoad();
changeRuleset(0);
AddAssert("two columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 2);
AddStep("unset filter", () => modSelectScreen.IsValidMod = _ => true);
AddAssert("all columns visible", () => this.ChildrenOfType<ModColumn>().All(col => col.IsPresent));
AddStep("filter out everything", () => modSelectScreen.IsValidMod = _ => false);
AddAssert("no columns visible", () => this.ChildrenOfType<ModColumn>().All(col => !col.IsPresent));
AddStep("hide", () => modSelectScreen.Hide());
AddStep("set filter for 3 columns", () => modSelectScreen.IsValidMod = mod => mod.Type == ModType.DifficultyReduction
|| mod.Type == ModType.Automation
|| mod.Type == ModType.Conversion);
AddStep("show", () => modSelectScreen.Show());
AddUntilStep("3 columns visible", () => this.ChildrenOfType<ModColumn>().Count(col => col.IsPresent) == 3);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded",
() => modSelectScreen.ChildrenOfType<ModColumn>().Any() && modSelectScreen.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded));

View File

@ -1,15 +1,18 @@
// 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.Game.Skinning;
namespace osu.Game.Screens.Play
namespace osu.Game.Audio
{
/// <summary>
/// Allows a component to disable sample playback dynamically as required.
/// Handled by <see cref="PausableSkinnableSound"/>.
/// Automatically handled by <see cref="PausableSkinnableSound"/>.
/// May also be manually handled locally to particular components.
/// </summary>
[Cached]
public interface ISamplePlaybackDisabler
{
/// <summary>

View File

@ -153,7 +153,17 @@ namespace osu.Game.Beatmaps
}
};
Task.Run(() => cacheDownloadRequest.PerformAsync());
Task.Run(async () =>
{
try
{
await cacheDownloadRequest.PerformAsync();
}
catch
{
// Prevent throwing unobserved exceptions, as they will be logged from the network request to the log file anyway.
}
});
}
private bool checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmapInfo)

View File

@ -431,9 +431,10 @@ namespace osu.Game.Beatmaps.Formats
OmitFirstBarLine = omitFirstBarSignature,
};
bool isOsuRuleset = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
// scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments.
if (!isOsuRuleset)
int onlineRulesetID = beatmap.BeatmapInfo.Ruleset.OnlineID;
// osu!taiko and osu!mania use effect points rather than difficulty points for scroll speed adjustments.
if (onlineRulesetID == 1 || onlineRulesetID == 3)
effectPoint.ScrollSpeed = speedMultiplier;
addControlPoint(time, effectPoint, timingChange);

View File

@ -183,15 +183,15 @@ namespace osu.Game.Beatmaps.Formats
SampleControlPoint lastRelevantSamplePoint = null;
DifficultyControlPoint lastRelevantDifficultyPoint = null;
bool isOsuRuleset = onlineRulesetID == 0;
// In osu!taiko and osu!mania, a scroll speed is stored as "slider velocity" in legacy formats.
// In that case, a scrolling speed change is a global effect and per-hit object difficulty control points are ignored.
bool scrollSpeedEncodedAsSliderVelocity = onlineRulesetID == 1 || onlineRulesetID == 3;
// iterate over hitobjects and pull out all required sample and difficulty changes
extractDifficultyControlPoints(beatmap.HitObjects);
extractSampleControlPoints(beatmap.HitObjects);
// handle scroll speed, which is stored as "slider velocity" in legacy formats.
// this is relevant for scrolling ruleset beatmaps.
if (!isOsuRuleset)
if (scrollSpeedEncodedAsSliderVelocity)
{
foreach (var point in legacyControlPoints.EffectPoints)
legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed });
@ -242,7 +242,7 @@ namespace osu.Game.Beatmaps.Formats
IEnumerable<DifficultyControlPoint> collectDifficultyControlPoints(IEnumerable<HitObject> hitObjects)
{
if (!isOsuRuleset)
if (scrollSpeedEncodedAsSliderVelocity)
yield break;
foreach (var hitObject in hitObjects)

View File

@ -100,10 +100,6 @@ namespace osu.Game.Overlays.Dialog
}
}
// We always want dialogs to show their appear animation, so we request they start hidden.
// Normally this would not be required, but is here due to the manual Show() call that occurs before LoadComplete().
protected override bool StartHidden => true;
protected PopupDialog()
{
RelativeSizeAxes = Axes.Both;
@ -272,7 +268,7 @@ namespace osu.Game.Overlays.Dialog
protected override void PopOut()
{
if (!actionInvoked && content.IsPresent)
if (!actionInvoked)
// In the case a user did not choose an action before a hide was triggered, press the last button.
// This is presumed to always be a sane default "cancel" action.
buttonsContainer.Last().TriggerClick();

View File

@ -55,8 +55,18 @@ namespace osu.Game.Overlays.Mods
}
}
/// <summary>
/// Determines whether this column should accept user input.
/// </summary>
public Bindable<bool> Active = new BindableBool(true);
private readonly Bindable<bool> allFiltered = new BindableBool();
/// <summary>
/// True if all of the panels in this column have been filtered out by the current <see cref="Filter"/>.
/// </summary>
public IBindable<bool> AllFiltered => allFiltered;
/// <summary>
/// List of mods marked as selected in this column.
/// </summary>
@ -339,6 +349,8 @@ namespace osu.Game.Overlays.Mods
panel.ApplyFilter(Filter);
}
allFiltered.Value = panelFlow.All(panel => panel.Filtered.Value);
if (toggleAllCheckbox != null && !SelectionAnimationRunning)
{
toggleAllCheckbox.Alpha = panelFlow.Any(panel => !panel.Filtered.Value) ? 1 : 0;

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.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -12,6 +14,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -21,8 +24,6 @@ using osu.Game.Rulesets.UI;
using osuTK;
using osuTK.Input;
#nullable enable
namespace osu.Game.Overlays.Mods
{
public class ModPanel : OsuClickableContainer
@ -50,6 +51,7 @@ namespace osu.Game.Overlays.Mods
private Colour4 activeColour;
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool();
private Sample? sampleOff;
private Sample? sampleOn;
@ -139,13 +141,16 @@ namespace osu.Game.Overlays.Mods
Action = Active.Toggle;
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, OsuColour colours, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
activeColour = colours.ForModType(Mod.Type);
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
}
protected override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -166,6 +171,9 @@ namespace osu.Game.Overlays.Mods
private void playStateChangeSamples()
{
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else

View File

@ -15,6 +15,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Lists;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -27,7 +28,7 @@ using osuTK.Input;
namespace osu.Game.Overlays.Mods
{
public abstract class ModSelectScreen : ShearedOverlayContainer
public abstract class ModSelectScreen : ShearedOverlayContainer, ISamplePlaybackDisabler
{
protected const int BUTTON_WIDTH = 200;
@ -128,7 +129,6 @@ namespace osu.Game.Overlays.Mods
Shear = new Vector2(SHEAR, 0),
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Spacing = new Vector2(10, 0),
Margin = new MarginPadding { Horizontal = 70 },
Children = new[]
{
@ -188,6 +188,8 @@ namespace osu.Game.Overlays.Mods
{
base.LoadComplete();
State.BindValueChanged(_ => samplePlaybackDisabled.Value = State.Value == Visibility.Hidden, true);
((IBindable<IReadOnlyList<Mod>>)modSettingsArea.SelectedMods).BindTo(SelectedMods);
SelectedMods.BindValueChanged(val =>
@ -205,6 +207,14 @@ namespace osu.Game.Overlays.Mods
customisationVisible.BindValueChanged(_ => updateCustomisationVisualState(), true);
updateAvailableMods();
// Start scrolled slightly to the right to give the user a sense that
// there is more horizontal content available.
ScheduleAfterChildren(() =>
{
columnScroll.ScrollTo(200, false);
columnScroll.ScrollToStart();
});
}
/// <summary>
@ -226,12 +236,21 @@ namespace osu.Game.Overlays.Mods
}
private ColumnDimContainer createModColumnContent(ModType modType, Key[]? toggleKeys = null)
=> new ColumnDimContainer(CreateModColumn(modType, toggleKeys))
{
var column = CreateModColumn(modType, toggleKeys).With(column =>
{
column.Filter = IsValidMod;
// spacing applied here rather than via `columnFlow.Spacing` to avoid uneven gaps when some of the columns are hidden.
column.Margin = new MarginPadding { Right = 10 };
});
return new ColumnDimContainer(column)
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
RequestScroll = column => columnScroll.ScrollIntoView(column, extraScroll: 140)
RequestScroll = col => columnScroll.ScrollIntoView(col, extraScroll: 140),
};
}
private ShearedButton[] createDefaultFooterButtons()
=> new[]
@ -340,6 +359,8 @@ namespace osu.Game.Overlays.Mods
#region Transition handling
private const float distance = 700;
protected override void PopIn()
{
const double fade_in_duration = 400;
@ -351,13 +372,26 @@ namespace osu.Game.Overlays.Mods
.FadeIn(fade_in_duration / 2, Easing.OutQuint)
.ScaleTo(1, fade_in_duration, Easing.OutElastic);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
columnFlow[i].Column
.TopLevelContent
.Delay(i * 30)
.MoveToY(0, fade_in_duration, Easing.OutQuint)
.FadeIn(fade_in_duration, Easing.OutQuint);
var column = columnFlow[i].Column;
double delay = column.AllFiltered.Value ? 0 : nonFilteredColumnCount * 30;
double duration = column.AllFiltered.Value ? 0 : fade_in_duration;
float startingYPosition = 0;
if (!column.AllFiltered.Value)
startingYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.TopLevelContent
.MoveToY(startingYPosition)
.Delay(delay)
.MoveToY(0, duration, Easing.OutQuint)
.FadeIn(duration, Easing.OutQuint);
if (!column.AllFiltered.Value)
nonFilteredColumnCount += 1;
}
}
@ -371,16 +405,24 @@ namespace osu.Game.Overlays.Mods
.FadeOut(fade_out_duration / 2, Easing.OutQuint)
.ScaleTo(0.75f, fade_out_duration, Easing.OutQuint);
int nonFilteredColumnCount = 0;
for (int i = 0; i < columnFlow.Count; i++)
{
const float distance = 700;
var column = columnFlow[i].Column;
double duration = column.AllFiltered.Value ? 0 : fade_out_duration;
float newYPosition = 0;
if (!column.AllFiltered.Value)
newYPosition = nonFilteredColumnCount % 2 == 0 ? -distance : distance;
column.FlushPendingSelections();
column.TopLevelContent
.MoveToY(i % 2 == 0 ? -distance : distance, fade_out_duration, Easing.OutQuint)
.FadeOut(fade_out_duration, Easing.OutQuint);
.MoveToY(newYPosition, duration, Easing.OutQuint)
.FadeOut(duration, Easing.OutQuint);
if (!column.AllFiltered.Value)
nonFilteredColumnCount += 1;
}
}
@ -393,35 +435,50 @@ namespace osu.Game.Overlays.Mods
if (e.Repeat)
return false;
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
if (e.Action == GlobalAction.Back)
{
if (customisationVisible.Value)
customisationVisible.Value = false;
else
backButton.TriggerClick();
return true;
}
switch (e.Action)
{
case GlobalAction.Back:
// Pressing the back binding should only go back one step at a time.
hideOverlay(false);
return true;
// This is handled locally here because this overlay is being registered at the game level
// and therefore takes away keyboard focus from the screen stack.
case GlobalAction.ToggleModSelection:
case GlobalAction.Select:
{
if (customisationVisible.Value)
customisationVisible.Value = false;
Hide();
// Pressing toggle or select should completely hide the overlay in one shot.
hideOverlay(true);
return true;
}
}
default:
return base.OnPressed(e);
return base.OnPressed(e);
void hideOverlay(bool immediate)
{
if (customisationVisible.Value)
{
Debug.Assert(customisationButton != null);
customisationButton.TriggerClick();
if (!immediate)
return;
}
backButton.TriggerClick();
}
}
#endregion
#region Sample playback control
private readonly Bindable<bool> samplePlaybackDisabled = new BindableBool(true);
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled;
#endregion
/// <summary>
/// Manages horizontal scrolling of mod columns, along with the "active" states of each column based on visibility.
/// </summary>
@ -531,17 +588,20 @@ namespace osu.Game.Overlays.Mods
protected override void LoadComplete()
{
base.LoadComplete();
Active.BindValueChanged(_ => updateDim(), true);
Active.BindValueChanged(_ => updateState());
Column.AllFiltered.BindValueChanged(_ => updateState(), true);
FinishTransforms();
}
protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate || Column.SelectionAnimationRunning;
private void updateDim()
private void updateState()
{
Colour4 targetColour;
if (Active.Value)
Column.Alpha = Column.AllFiltered.Value ? 0 : 1;
if (Column.Active.Value)
targetColour = Colour4.White;
else
targetColour = IsHovered ? colours.GrayC : colours.Gray8;
@ -560,14 +620,14 @@ namespace osu.Game.Overlays.Mods
protected override bool OnHover(HoverEvent e)
{
base.OnHover(e);
updateDim();
updateState();
return Active.Value;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateDim();
updateState();
}
}

View File

@ -39,6 +39,18 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
bool UserPlayable { get; }
/// <summary>
/// Whether this mod is valid for multiplayer matches.
/// Should be <c>false</c> for mods that make gameplay duration dependent on user input (e.g. <see cref="ModAdaptiveSpeed"/>).
/// </summary>
bool ValidForMultiplayer { get; }
/// <summary>
/// Whether this mod is valid as a free mod in multiplayer matches.
/// Should be <c>false</c> for mods that affect the gameplay duration (e.g. <see cref="ModRateAdjust"/> and <see cref="ModTimeRamp"/>).
/// </summary>
bool ValidForMultiplayerAsFreeMod { get; }
/// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>

View File

@ -94,6 +94,12 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool UserPlayable => true;
[JsonIgnore]
public virtual bool ValidForMultiplayer => true;
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
public virtual bool Ranked => false;

View File

@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 1;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp) };
[SettingSource("Initial rate", "The starting speed of the track")]

View File

@ -25,6 +25,8 @@ namespace osu.Game.Rulesets.Mods
public bool RestartOnFail => false;
public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };

View File

@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Mods
{
public abstract class ModRateAdjust : Mod, IApplicableToRate
{
public override bool ValidForMultiplayerAsFreeMod => false;
public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(ITrack track)

View File

@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public abstract BindableBool AdjustPitch { get; }
public override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModAdaptiveSpeed) };
public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x";

View File

@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0;
public override bool UserPlayable => false;
public override bool ValidForMultiplayer => false;
public override bool ValidForMultiplayerAsFreeMod => false;
public override ModType Type => ModType.System;

View File

@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
@ -50,7 +51,6 @@ using osuTK.Input;
namespace osu.Game.Screens.Edit
{
[Cached(typeof(IBeatSnapProvider))]
[Cached(typeof(ISamplePlaybackDisabler))]
[Cached]
public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler<GlobalAction>, IKeyBindingHandler<PlatformAction>, IBeatSnapProvider, ISamplePlaybackDisabler
{

View File

@ -65,6 +65,8 @@ namespace osu.Game.Screens.OnlinePlay
public readonly PlaylistItem Item;
public bool IsSelectedItem => SelectedItem.Value?.ID == Item.ID;
private readonly DelayedLoadWrapper onScreenLoader = new DelayedLoadWrapper(Empty) { RelativeSizeAxes = Axes.Both };
private readonly IBindable<bool> valid = new Bindable<bool>();
@ -128,12 +130,10 @@ namespace osu.Game.Screens.OnlinePlay
SelectedItem.BindValueChanged(selected =>
{
bool isCurrent = selected.NewValue == Model;
if (!valid.Value)
{
// Don't allow selection when not valid.
if (isCurrent)
if (IsSelectedItem)
{
SelectedItem.Value = selected.OldValue;
}
@ -142,7 +142,7 @@ namespace osu.Game.Screens.OnlinePlay
return;
}
maskingContainer.BorderThickness = isCurrent ? 5 : 0;
maskingContainer.BorderThickness = IsSelectedItem ? 5 : 0;
}, true);
valid.BindValueChanged(_ => Scheduler.AddOnce(refresh));

View File

@ -95,6 +95,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust);
protected override bool IsValidMod(Mod mod) => base.IsValidMod(mod) && mod.ValidForMultiplayer;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && mod.ValidForMultiplayerAsFreeMod;
}
}

View File

@ -16,6 +16,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
@ -37,7 +38,6 @@ using osuTK.Graphics;
namespace osu.Game.Screens.Play
{
[Cached]
[Cached(typeof(ISamplePlaybackDisabler))]
public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler, ILocalUserPlayInfo
{
/// <summary>

View File

@ -85,7 +85,6 @@ namespace osu.Game.Screens.Ranking
InternalChild = scroll = new Scroll
{
RelativeSizeAxes = Axes.Both,
HandleScroll = () => expandedPanel?.IsHovered != true, // handle horizontal scroll only when not hovering the expanded panel.
Child = flow = new Flow
{
Anchor = Anchor.Centre,
@ -359,11 +358,6 @@ namespace osu.Game.Screens.Ranking
/// </summary>
public float? InstantScrollTarget;
/// <summary>
/// Whether this container should handle scroll trigger events.
/// </summary>
public Func<bool> HandleScroll;
protected override void UpdateAfterChildren()
{
if (InstantScrollTarget != null)
@ -374,10 +368,6 @@ namespace osu.Game.Screens.Ranking
base.UpdateAfterChildren();
}
public override bool HandlePositionalInput => HandleScroll();
public override bool HandleNonPositionalInput => HandleScroll();
}
}
}

View File

@ -8,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Screens.Play;
namespace osu.Game.Skinning
{

View File

@ -106,22 +106,69 @@ namespace osu.Game.Utils
}
/// <summary>
/// Check the provided combination of mods are valid for a local gameplay session.
/// Checks that all <see cref="Mod"/>s in a combination are valid for a local gameplay session.
/// </summary>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Can be null if all mods were valid.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidForGameplay(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
// exclude multi mods from compatibility checks.
// the loop below automatically marks all multi mods as not valid for gameplay anyway.
CheckCompatibleSet(mods.Where(m => !(m is MultiMod)), out invalidMods);
// checking compatibility of multi mods would try to flatten them and return incompatible mods.
// in gameplay context, we never want MultiMod selected in the first place, therefore check against it first.
if (!checkValid(mods, m => !(m is MultiMod), out invalidMods))
return false;
if (!CheckCompatibleSet(mods, out invalidMods))
return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "required mods" in a multiplayer match session.
/// </summary>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidRequiredModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
// checking compatibility of multi mods would try to flatten them and return incompatible mods.
// in gameplay context, we never want MultiMod selected in the first place, therefore check against it first.
if (!checkValid(mods, m => !(m is MultiMod), out invalidMods))
return false;
if (!CheckCompatibleSet(mods, out invalidMods))
return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayer, out invalidMods);
}
/// <summary>
/// Checks that all <see cref="Mod"/>s in a combination are valid as "free mods" in a multiplayer match session.
/// </summary>
/// <remarks>
/// Note that this does not check compatibility between mods,
/// given that the passed mods are expected to be the ones to be allowed for the multiplayer match,
/// not to be confused with the list of mods the user currently has selected for the multiplayer match.
/// </remarks>
/// <param name="mods">The mods to check.</param>
/// <param name="invalidMods">Invalid mods, if any were found. Will be null if all mods were valid.</param>
/// <returns>Whether the input mods were all valid. If false, <paramref name="invalidMods"/> will contain all invalid entries.</returns>
public static bool CheckValidFreeModsForMultiplayer(IEnumerable<Mod> mods, [NotNullWhen(false)] out List<Mod>? invalidMods)
=> checkValid(mods, m => m.Type != ModType.System && m.HasImplementation && m.ValidForMultiplayerAsFreeMod && !(m is MultiMod), out invalidMods);
private static bool checkValid(IEnumerable<Mod> mods, Predicate<Mod> valid, [NotNullWhen(false)] out List<Mod>? invalidMods)
{
mods = mods.ToArray();
invalidMods = null;
foreach (var mod in mods)
{
if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod)
if (!valid(mod))
{
invalidMods ??= new List<Mod>();
invalidMods.Add(mod);

View File

@ -24,7 +24,7 @@ namespace osu.Game.Utils
var options = new SentryOptions
{
Dsn = "https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255",
Dsn = "https://ad9f78529cef40ac874afb95a9aca04e@sentry.ppy.sh/2",
Release = game.Version
};

View File

@ -35,7 +35,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
<PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" />

View File

@ -61,7 +61,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.509.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.422.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.430.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.509.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />