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:
commit
1c63f1f89d
204
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
Normal file
204
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModTouchDevice.cs
Normal 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,
|
||||
})
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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[]
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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", () =>
|
||||
{
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
72
osu.Game/Input/TouchInputInterceptor.cs
Normal file
72
osu.Game/Input/TouchInputInterceptor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -407,6 +407,8 @@ namespace osu.Game
|
||||
})
|
||||
});
|
||||
|
||||
base.Content.Add(new TouchInputInterceptor());
|
||||
|
||||
KeyBindingStore = new RealmKeyBindingStore(realm, keyCombinationProvider);
|
||||
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
|
||||
|
||||
|
@ -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)!
|
||||
}));
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
||||
|
22
osu.Game/Rulesets/Mods/ModTouchDevice.cs
Normal file
22
osu.Game/Rulesets/Mods/ModTouchDevice.cs
Normal 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) };
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
60
osu.Game/Screens/Play/PlayerTouchInputDetector.cs
Normal file
60
osu.Game/Screens/Play/PlayerTouchInputDetector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -279,6 +279,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new SongSelectTouchInputDetector()
|
||||
});
|
||||
|
||||
if (ShowFooter)
|
||||
|
69
osu.Game/Screens/Select/SongSelectTouchInputDetector.cs
Normal file
69
osu.Game/Screens/Select/SongSelectTouchInputDetector.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user