1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:18:22 +08:00

Merge pull request #25348 from bdach/less-basic-touchscreen-detection

Implement basic touchscreen detection
This commit is contained in:
Dean Herbert 2023-11-10 18:01:40 +09:00 committed by GitHub
commit 1c63f1f89d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 610 additions and 29 deletions

View File

@ -0,0 +1,204 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Input;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModTouchDevice : RateAdjustedBeatmapTestScene
{
[Resolved]
private SessionStatics statics { get; set; } = null!;
private ScoreAccessibleSoloPlayer currentPlayer = null!;
private readonly ManualClock manualClock = new ManualClock { Rate = 0 };
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
=> new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(manualClock), Audio);
[BackgroundDependencyLoader]
private void load()
{
Add(new TouchInputInterceptor());
}
public override void SetUpSteps()
{
AddStep("reset static", () => statics.SetValue(Static.TouchInputActive, false));
base.SetUpSteps();
}
[Test]
public void TestUserAlreadyHasTouchDeviceActive()
{
loadPlayer();
// it is presumed that a previous screen (i.e. song select) will set this up
AddStep("set up touchscreen user", () =>
{
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
statics.SetValue(Static.TouchInputActive, true);
});
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch circle", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchDuringBreak()
{
loadPlayer();
AddStep("seek to 2000", () => currentPlayer.GameplayClockContainer.Seek(2000));
AddUntilStep("wait until 2000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(2000));
AddUntilStep("wait until break entered", () => currentPlayer.IsBreakTime.Value);
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchMiss()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 200", () => currentPlayer.GameplayClockContainer.Seek(200));
AddUntilStep("wait until 200", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(200));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestIncompatibleModActive()
{
loadPlayer();
// this is only a veneer of enabling autopilot as having it actually active from the start is annoying to make happen
// given the tests' structure.
AddStep("enable autopilot", () => currentPlayer.Score.ScoreInfo.Mods = new Mod[] { new OsuModAutopilot() });
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestSecondObjectTouched()
{
loadPlayer();
// ensure mouse is active (and that it's not suppressed due to touches in previous tests)
AddStep("click mouse", () => InputManager.Click(MouseButton.Left));
AddStep("seek to 0", () => currentPlayer.GameplayClockContainer.Seek(0));
AddUntilStep("wait until 0", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(0));
AddStep("click circle", () =>
{
InputManager.MoveMouseTo(currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Left);
});
AddAssert("touch device mod not activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.None.InstanceOf<OsuModTouchDevice>());
AddStep("seek to 5000", () => currentPlayer.GameplayClockContainer.Seek(5000));
AddUntilStep("wait until 5000", () => currentPlayer.GameplayClockContainer.CurrentTime, () => Is.GreaterThanOrEqualTo(5000));
AddStep("touch playfield", () =>
{
var touch = new Touch(TouchSource.Touch1, currentPlayer.DrawableRuleset.Playfield.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
private void loadPlayer()
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new OsuBeatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
},
},
Breaks =
{
new BreakPeriod(2000, 3000)
}
});
var p = new ScoreAccessibleSoloPlayer();
LoadScreen(currentPlayer = p);
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
}
private partial class ScoreAccessibleSoloPlayer : SoloPlayer
{
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleSoloPlayer()
: base(new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -33,7 +33,8 @@ namespace osu.Game.Rulesets.Osu.Mods
typeof(ModNoFail),
typeof(ModAutoplay),
typeof(OsuModMagnetised),
typeof(OsuModRepel)
typeof(OsuModRepel),
typeof(ModTouchDevice)
};
public bool PerformFail() => false;

View File

@ -1,18 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
using System;
using System.Linq;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModTouchDevice : Mod
public class OsuModTouchDevice : ModTouchDevice
{
public override string Name => "Touch Device";
public override string Acronym => "TD";
public override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public override double ScoreMultiplier => 1;
public override ModType Type => ModType.System;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
}
}

View File

@ -147,11 +147,11 @@ namespace osu.Game.Tests.Mods
new Mod[] { new OsuModDeflate(), new OsuModApproachDifferent() },
new[] { typeof(OsuModDeflate), typeof(OsuModApproachDifferent) }
},
// system mod.
// system mod not applicable in lazer.
new object[]
{
new Mod[] { new OsuModHidden(), new OsuModTouchDevice() },
new[] { typeof(OsuModTouchDevice) }
new Mod[] { new OsuModHidden(), new ModScoreV2() },
new[] { typeof(ModScoreV2) }
},
// multi mod.
new object[]

View File

@ -133,6 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private bool assertAllAvailableModsSelected()
{
var allAvailableMods = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList();

View File

@ -12,6 +12,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@ -835,6 +836,110 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog is ConfirmExitDialog);
}
[Test]
public void TestTouchScreenDetectionAtSongSelect()
{
AddStep("touch logo", () =>
{
var button = Game.ChildrenOfType<OsuLogo>().Single();
var touch = new Touch(TouchSource.Touch1, button.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddAssert("touch screen detected active", () => Game.Dependencies.Get<SessionStatics>().Get<bool>(Static.TouchInputActive), () => Is.True);
AddStep("click settings button", () =>
{
var button = Game.ChildrenOfType<MainMenuButton>().Last();
InputManager.MoveMouseTo(button);
InputManager.Click(MouseButton.Left);
});
AddAssert("touch screen detected inactive", () => Game.Dependencies.Get<SessionStatics>().Get<bool>(Static.TouchInputActive), () => Is.False);
AddStep("close settings sidebar", () => InputManager.Key(Key.Escape));
Screens.Select.SongSelect songSelect = null;
AddRepeatStep("go to solo", () => InputManager.Key(Key.P), 3);
AddUntilStep("wait for song select", () => (songSelect = Game.ScreenStack.CurrentScreen as Screens.Select.SongSelect) != null);
AddUntilStep("wait for beatmap sets loaded", () => songSelect.BeatmapSetsLoaded);
AddStep("switch to osu! ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.LControl);
});
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("switch to mania ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number4);
InputManager.ReleaseKey(Key.LControl);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("touch beatmap wedge", () =>
{
var wedge = Game.ChildrenOfType<BeatmapInfoWedge>().Single();
var touch = new Touch(TouchSource.Touch2, wedge.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
AddStep("switch to osu! ruleset", () =>
{
InputManager.PressKey(Key.LControl);
InputManager.Key(Key.Number1);
InputManager.ReleaseKey(Key.LControl);
});
AddUntilStep("touch device mod activated", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("click beatmap wedge", () =>
{
InputManager.MoveMouseTo(Game.ChildrenOfType<BeatmapInfoWedge>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("touch device mod not activated", () => Game.SelectedMods.Value, () => Has.None.InstanceOf<ModTouchDevice>());
}
[Test]
public void TestTouchScreenDetectionInGame()
{
PushAndConfirm(() => new TestPlaySongSelect());
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("select", () => InputManager.Key(Key.Enter));
Player player = null;
AddUntilStep("wait for player", () =>
{
DismissAnyNotifications();
return (player = Game.ScreenStack.CurrentScreen as Player) != null;
});
AddUntilStep("wait for track playing", () => Game.Beatmap.Value.Track.IsRunning);
AddStep("touch", () =>
{
var touch = new Touch(TouchSource.Touch2, Game.ScreenSpaceDrawQuad.Centre);
InputManager.BeginTouch(touch);
InputManager.EndTouch(touch);
});
AddUntilStep("touch device mod added to score", () => player.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<ModTouchDevice>());
AddStep("exit player", () => player.Exit());
AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
}
private Func<Player> playToResults()
{
var player = playToCompletion();

View File

@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestAddingFlow()
public void TestAddingFlow([Values] bool withSystemModActive)
{
ModPresetColumn modPresetColumn = null!;
@ -181,7 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddAssert("add preset button disabled", () => !this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("set mods", () => SelectedMods.Value = new Mod[] { new OsuModDaycore(), new OsuModClassic() });
AddStep("set mods", () =>
{
var newMods = new Mod[] { new OsuModDaycore(), new OsuModClassic() };
if (withSystemModActive)
newMods = newMods.Append(new OsuModTouchDevice()).ToArray();
SelectedMods.Value = newMods;
});
AddAssert("add preset button enabled", () => this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("click add preset button", () =>
@ -209,6 +215,9 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddUntilStep("preset creation occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 4);
AddAssert("preset has correct mods",
() => this.ChildrenOfType<ModPresetPanel>().Single(panel => panel.Preset.Value.Name == "new preset").Preset.Value.Mods,
() => Has.Count.EqualTo(2));
AddStep("click add preset button", () =>
{

View File

@ -86,6 +86,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
// system mods are not included in presets.
AddStep("set mods to HR+DT+TD", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime(), new OsuModTouchDevice() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
}
[Test]
@ -113,6 +117,10 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
AddStep("set system mod", () => SelectedMods.Value = new[] { new OsuModTouchDevice() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModTouchDevice(), new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
}
private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)

View File

@ -3,7 +3,9 @@
#nullable disable
using osu.Framework;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
@ -24,6 +26,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
}
/// <summary>
@ -63,6 +66,12 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary>
LastModSelectPanelSamplePlaybackTime
LastModSelectPanelSamplePlaybackTime,
/// <summary>
/// Whether the last positional input received was a touch input.
/// Used in touchscreen detection scenarios (<see cref="TouchInputInterceptor"/>).
/// </summary>
TouchInputActive,
}
}

View File

@ -0,0 +1,72 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Input.StateChanges;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osuTK;
using osuTK.Input;
namespace osu.Game.Input
{
/// <summary>
/// Intercepts all positional input events and sets the appropriate <see cref="Static.TouchInputActive"/> value
/// for consumption by particular game screens.
/// </summary>
public partial class TouchInputInterceptor : Component
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly BindableBool touchInputActive = new BindableBool();
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
statics.BindWith(Static.TouchInputActive, touchInputActive);
}
protected override bool Handle(UIEvent e)
{
bool touchInputWasActive = touchInputActive.Value;
switch (e)
{
case MouseEvent:
if (e.CurrentState.Mouse.LastSource is not ISourcedFromTouch)
{
if (touchInputWasActive)
Logger.Log($@"Touch input deactivated due to received {e.GetType().ReadableName()}", LoggingTarget.Input);
touchInputActive.Value = false;
}
break;
case TouchEvent:
if (!touchInputWasActive)
Logger.Log($@"Touch input activated due to received {e.GetType().ReadableName()}", LoggingTarget.Input);
touchInputActive.Value = true;
break;
case KeyDownEvent keyDown:
if (keyDown.Key == Key.T && keyDown.ControlPressed && keyDown.ShiftPressed)
debugToggleTouchInputActive();
break;
}
return false;
}
[Conditional("TOUCH_INPUT_DEBUG")]
private void debugToggleTouchInputActive()
{
Logger.Log($@"Debug-toggling touch input to {(touchInputActive.Value ? @"inactive" : @"active")}", LoggingTarget.Information, LogLevel.Important);
touchInputActive.Toggle();
}
}
}

View File

@ -407,6 +407,8 @@ namespace osu.Game
})
});
base.Content.Add(new TouchInputInterceptor());
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);

View File

@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Mods
{
Name = nameTextBox.Current.Value,
Description = descriptionTextBox.Current.Value,
Mods = selectedMods.Value.ToArray(),
Mods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToArray(),
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)!
}));

View File

@ -153,7 +153,7 @@ namespace osu.Game.Overlays.Mods
private void useCurrentMods()
{
saveableMods = selectedMods.Value.ToHashSet();
saveableMods = selectedMods.Value.Where(mod => mod.Type != ModType.System).ToHashSet();
updateState();
}
@ -168,7 +168,7 @@ namespace osu.Game.Overlays.Mods
if (!selectedMods.Value.Any())
return false;
return !saveableMods.SetEquals(selectedMods.Value);
return !saveableMods.SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System));
}
private void save()

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
@ -56,17 +55,14 @@ namespace osu.Game.Overlays.Mods
protected override void Select()
{
// if the preset is not active at the point of the user click, then set the mods using the preset directly, discarding any previous selections,
// which will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.ToArray();
var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System);
// will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray();
}
protected override void Deselect()
{
// if the preset is active when the user has clicked it, then it means that the set of active mods is exactly equal to the set of mods in the preset
// (there are no other active mods than what the preset specifies, and the mod settings match exactly).
// therefore it's safe to just clear selected mods, since it will have the effect of toggling the preset off.
selectedMods.Value = Array.Empty<Mod>();
selectedMods.Value = selectedMods.Value.Except(Preset.Value.Mods).ToArray();
}
private void selectedModsChanged()
@ -79,7 +75,7 @@ namespace osu.Game.Overlays.Mods
private void updateActiveState()
{
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value);
Active.Value = new HashSet<Mod>(Preset.Value.Mods).SetEquals(selectedMods.Value.Where(mod => mod.Type != ModType.System));
}
#region Filtering support

View File

@ -41,6 +41,7 @@ namespace osu.Game.Overlays.Mods
private void updateEnabledState()
{
Enabled.Value = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.Any(modState => !modState.Active.Value && modState.Visible);
}

View File

@ -59,6 +59,13 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
bool ValidForMultiplayerAsFreeMod { get; }
/// <summary>
/// Indicates that this mod is always permitted in scenarios wherein a user is submitting a score regardless of other circumstances.
/// Intended for mods that are informational in nature and do not really affect gameplay by themselves,
/// but are more of a gauge of increased/decreased difficulty due to the user's configuration (e.g. <see cref="ModTouchDevice"/>).
/// </summary>
bool AlwaysValidForSubmission { get; }
/// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>

View File

@ -156,6 +156,10 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
public virtual bool ValidForMultiplayerAsFreeMod => true;
/// <inheritdoc/>
[JsonIgnore]
public virtual bool AlwaysValidForSubmission => false;
/// <summary>
/// Whether this mod requires configuration to apply changes to the game.
/// </summary>

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => false;
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed) };
public override Type[] IncompatibleMods => new[] { typeof(ModCinema), typeof(ModRelax), typeof(ModAdaptiveSpeed), typeof(ModTouchDevice) };
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;

View File

@ -0,0 +1,22 @@
// 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.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
public class ModTouchDevice : Mod, IApplicableMod
{
public sealed override string Name => "Touch Device";
public sealed override string Acronym => "TD";
public sealed override IconUsage? Icon => OsuIcon.PlayStyleTouch;
public sealed override LocalisableString Description => "Automatically applied to plays on devices with a touchscreen.";
public sealed override double ScoreMultiplier => 1;
public sealed override ModType Type => ModType.System;
public sealed override bool AlwaysValidForSubmission => true;
public override Type[] IncompatibleMods => new[] { typeof(ICreateReplayData) };
}
}

View File

@ -204,6 +204,8 @@ namespace osu.Game.Rulesets
public ModAutoplay? GetAutoplayMod() => CreateMod<ModAutoplay>();
public ModTouchDevice? GetTouchDeviceMod() => CreateMod<ModTouchDevice>();
/// <summary>
/// Create a transformer which adds lookups specific to a ruleset to skin sources.
/// </summary>

View File

@ -0,0 +1,60 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Screens.Play
{
public partial class PlayerTouchInputDetector : Component
{
[Resolved]
private Player player { get; set; } = null!;
[Resolved]
private GameplayState gameplayState { get; set; } = null!;
private IBindable<bool> touchActive = new BindableBool();
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
touchActive = statics.GetBindable<bool>(Static.TouchInputActive);
touchActive.BindValueChanged(_ => updateState());
}
private void updateState()
{
if (!touchActive.Value)
return;
if (gameplayState.HasPassed || gameplayState.HasFailed || gameplayState.HasQuit)
return;
if (gameplayState.Score.ScoreInfo.Mods.OfType<ModTouchDevice>().Any())
return;
if (player.IsBreakTime.Value)
return;
var touchDeviceMod = gameplayState.Ruleset.GetTouchDeviceMod();
if (touchDeviceMod == null)
return;
var candidateMods = player.Score.ScoreInfo.Mods.Append(touchDeviceMod).ToArray();
if (!ModUtils.CheckCompatibleSet(candidateMods, out _))
return;
// `Player` (probably rightly so) assumes immutability of mods,
// so this will not be shown immediately on the mod display in the top right.
// if this is to change, the mod immutability should be revisited.
player.Score.ScoreInfo.Mods = candidateMods;
}
}
}

View File

@ -44,6 +44,18 @@ namespace osu.Game.Screens.Play
{
}
[BackgroundDependencyLoader]
private void load()
{
if (DrawableRuleset == null)
{
// base load must have failed (e.g. due to an unknown mod); bail.
return;
}
AddInternal(new PlayerTouchInputDetector());
}
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();

View File

@ -279,6 +279,7 @@ namespace osu.Game.Screens.Select
{
RelativeSizeAxes = Axes.Both,
},
new SongSelectTouchInputDetector()
});
if (ShowFooter)

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Utils;
namespace osu.Game.Screens.Select
{
public partial class SongSelectTouchInputDetector : Component
{
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; } = null!;
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
private IBindable<bool> touchActive = null!;
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{
touchActive = statics.GetBindable<bool>(Static.TouchInputActive);
}
protected override void LoadComplete()
{
base.LoadComplete();
ruleset.BindValueChanged(_ => Scheduler.AddOnce(updateState));
mods.BindValueChanged(_ => Scheduler.AddOnce(updateState));
mods.BindDisabledChanged(_ => Scheduler.AddOnce(updateState));
touchActive.BindValueChanged(_ => Scheduler.AddOnce(updateState));
updateState();
}
private void updateState()
{
if (mods.Disabled)
return;
var touchDeviceMod = ruleset.Value.CreateInstance().GetTouchDeviceMod();
if (touchDeviceMod == null)
return;
bool touchDeviceModEnabled = mods.Value.Any(mod => mod is ModTouchDevice);
if (touchActive.Value && !touchDeviceModEnabled)
{
var candidateMods = mods.Value.Append(touchDeviceMod).ToArray();
if (!ModUtils.CheckCompatibleSet(candidateMods, out _))
return;
mods.Value = candidateMods;
}
if (!touchActive.Value && touchDeviceModEnabled)
mods.Value = mods.Value.Where(mod => mod is not ModTouchDevice).ToArray();
}
}
}

View File

@ -121,7 +121,7 @@ namespace osu.Game.Utils
if (!CheckCompatibleSet(mods, out invalidMods))
return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods);
return checkValid(mods, m => m.HasImplementation, out invalidMods);
}
/// <summary>