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

Merge branch 'master' into skin-size-editing

This commit is contained in:
Bartłomiej Dach 2023-11-10 18:19:27 +09:00
commit c522a703eb
No known key found for this signature in database
37 changed files with 697 additions and 78 deletions

View File

@ -185,9 +185,11 @@ jobs:
- name: Add comment environment - name: Add comment environment
if: ${{ github.event_name == 'issue_comment' }} if: ${{ github.event_name == 'issue_comment' }}
env:
COMMENT_BODY: ${{ github.event.comment.body }}
run: | run: |
# Add comment environment # Add comment environment
echo '${{ github.event.comment.body }}' | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do echo $COMMENT_BODY | sed -r 's/\r$//' | grep -E '^\w+=' | while read -r line; do
opt=$(echo ${line} | cut -d '=' -f1) opt=$(echo ${line} | cut -d '=' -f1)
sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}" sed -i "s;^${opt}=.*$;${line};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
done done

View File

@ -11,6 +11,7 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays.Settings.Sections.Input;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Game.Utils; using osu.Game.Utils;
@ -97,6 +98,9 @@ namespace osu.Android
case AndroidJoystickHandler jh: case AndroidJoystickHandler jh:
return new AndroidJoystickSettings(jh); return new AndroidJoystickSettings(jh);
case AndroidTouchHandler th:
return new TouchSettings(th);
default: default:
return base.CreateSettingsSubsectionFor(handler); return base.CreateSettingsSubsectionFor(handler);
} }

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

@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.710442985146793d, 206, "diffcalc-test")] [TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(1.4386882251130073d, 45, "zero-length-sliders")] [TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 2, "very-fast-slider")] [TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(0.14102693012101306d, 1, "nan-slider")] [TestCase(0.14102693012101306d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9742952703071666d, 206, "diffcalc-test")] [TestCase(8.9742952703071666d, 239, "diffcalc-test")]
[TestCase(0.55071082800473514d, 2, "very-fast-slider")] [TestCase(1.743180218215227d, 54, "zero-length-sliders")]
[TestCase(1.743180218215227d, 45, "zero-length-sliders")] [TestCase(0.55071082800473514d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.710442985146793d, 239, "diffcalc-test")] [TestCase(6.710442985146793d, 239, "diffcalc-test")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")] [TestCase(1.4386882251130073d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Tests
if (hit) if (hit)
assertAllMaxJudgements(); assertAllMaxJudgements();
else else
AddAssert("Tracking dropped", assertMidSliderJudgementFail); assertMidSliderJudgementFail();
AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle); AddAssert("Head judgement is first", () => judgementResults.First().HitObject is SliderHeadCircle);
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_1 },
}); });
AddAssert("Tracking lost", assertMidSliderJudgementFail); assertMidSliderJudgementFail();
} }
/// <summary> /// <summary>
@ -278,7 +278,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_before_slider },
}); });
AddAssert("Tracking retained, sliderhead miss", assertHeadMissTailTracked); assertHeadMissTailTracked();
} }
/// <summary> /// <summary>
@ -302,7 +302,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
}); });
AddAssert("Tracking re-acquired", assertMidSliderJudgements); assertMidSliderJudgements();
} }
/// <summary> /// <summary>
@ -328,7 +328,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
}); });
AddAssert("Tracking lost", assertMidSliderJudgementFail); assertMidSliderJudgementFail();
} }
/// <summary> /// <summary>
@ -350,7 +350,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
}); });
AddAssert("Tracking acquired", assertMidSliderJudgements); assertMidSliderJudgements();
} }
/// <summary> /// <summary>
@ -373,7 +373,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_2 },
}); });
AddAssert("Tracking acquired", assertMidSliderJudgements); assertMidSliderJudgements();
} }
[Test] [Test]
@ -387,7 +387,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 },
}); });
AddAssert("Tracking acquired", assertMidSliderJudgements); assertMidSliderJudgements();
} }
/// <summary> /// <summary>
@ -412,7 +412,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 }, new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.LeftButton }, Time = time_during_slide_4 },
}); });
AddAssert("Tracking acquired", assertMidSliderJudgements); assertMidSliderJudgements();
} }
/// <summary> /// <summary>
@ -454,7 +454,7 @@ namespace osu.Game.Rulesets.Osu.Tests
new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.201f), Actions = { OsuAction.LeftButton }, Time = time_slider_end },
}); });
AddAssert("Tracking dropped", assertMidSliderJudgementFail); assertMidSliderJudgementFail();
} }
private void assertAllMaxJudgements() private void assertAllMaxJudgements()
@ -465,11 +465,21 @@ namespace osu.Game.Rulesets.Osu.Tests
}, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult)))); }, () => Is.EqualTo(judgementResults.Select(j => (j.HitObject, j.Judgement.MaxResult))));
} }
private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && !judgementResults.First().IsHit; private void assertHeadMissTailTracked()
{
AddAssert("Tracking retained", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
AddAssert("Slider head missed", () => judgementResults.First().IsHit, () => Is.False);
}
private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; private void assertMidSliderJudgements()
{
AddAssert("Tracking acquired", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.LargeTickHit));
}
private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; private void assertMidSliderJudgementFail()
{
AddAssert("Tracking lost", () => judgementResults[^2].Type, () => Is.EqualTo(HitResult.IgnoreMiss));
}
private void performTest(List<ReplayFrame> frames, Slider? slider = null, double? bpm = null, int? tickRate = null) private void performTest(List<ReplayFrame> frames, Slider? slider = null, double? bpm = null, int? tickRate = null)
{ {

View File

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

View File

@ -1,18 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // 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; using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModTouchDevice : Mod public class OsuModTouchDevice : ModTouchDevice
{ {
public override string Name => "Touch Device"; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray();
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;
} }
} }

View File

@ -135,6 +135,8 @@ namespace osu.Game.Rulesets.Osu.Objects
classicSliderBehaviour = value; classicSliderBehaviour = value;
if (HeadCircle != null) if (HeadCircle != null)
HeadCircle.ClassicSliderBehaviour = value; HeadCircle.ClassicSliderBehaviour = value;
if (TailCircle != null)
TailCircle.ClassicSliderBehaviour = value;
} }
} }
@ -218,6 +220,7 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = e.Time, StartTime = e.Time,
Position = EndPosition, Position = EndPosition,
StackHeight = StackHeight, StackHeight = StackHeight,
ClassicSliderBehaviour = ClassicSliderBehaviour,
}); });
break; break;
@ -273,9 +276,9 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
public override Judgement CreateJudgement() => ClassicSliderBehaviour public override Judgement CreateJudgement() => ClassicSliderBehaviour
// See logic in `DrawableSlider.CheckForResult()` // Final combo is provided by the slider itself - see logic in `DrawableSlider.CheckForResult()`
? new OsuJudgement() ? new OsuJudgement()
// Of note, this creates a combo discrepancy for non-classic-mod sliders (there is no combo increase for tail or slider judgement). // Final combo is provided by the tail circle - see `SliderTailCircle`
: new OsuIgnoreJudgement(); : new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -3,6 +3,8 @@
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
@ -43,5 +45,12 @@ namespace osu.Game.Rulesets.Osu.Objects
} }
protected override HitWindows CreateHitWindows() => HitWindows.Empty; protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderEndJudgement();
public class SliderEndJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
} }
} }

View File

@ -1,10 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SliderRepeat : SliderEndCircle public class SliderRepeat : SliderEndCircle
@ -13,12 +9,5 @@ namespace osu.Game.Rulesets.Osu.Objects
: base(slider) : base(slider)
{ {
} }
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
} }
} }

View File

@ -9,16 +9,28 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SliderTailCircle : SliderEndCircle public class SliderTailCircle : SliderEndCircle
{ {
/// <summary>
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// </summary>
public bool ClassicSliderBehaviour;
public SliderTailCircle(Slider slider) public SliderTailCircle(Slider slider)
: base(slider) : base(slider)
{ {
} }
public override Judgement CreateJudgement() => new SliderTailJudgement(); public override Judgement CreateJudgement() => ClassicSliderBehaviour ? new LegacyTailJudgement() : new TailJudgement();
public class SliderTailJudgement : OsuJudgement public class LegacyTailJudgement : OsuJudgement
{ {
public override HitResult MaxResult => HitResult.SmallTickHit; public override HitResult MaxResult => HitResult.SmallTickHit;
} }
public class TailJudgement : SliderEndJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
public override HitResult MinResult => HitResult.IgnoreMiss;
}
} }
} }

View File

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

View File

@ -11,9 +11,9 @@ namespace osu.Game.Tests.Rulesets.Scoring
[TestFixture] [TestFixture]
public class HitResultTest public class HitResultTest
{ {
[TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss })] [TestCase(new[] { HitResult.Perfect, HitResult.Great, HitResult.Good, HitResult.Ok, HitResult.Meh }, new[] { HitResult.Miss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss })] [TestCase(new[] { HitResult.LargeTickHit }, new[] { HitResult.LargeTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss })] [TestCase(new[] { HitResult.SmallTickHit }, new[] { HitResult.SmallTickMiss, HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })] [TestCase(new[] { HitResult.LargeBonus, HitResult.SmallBonus }, new[] { HitResult.IgnoreMiss })]
[TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })] [TestCase(new[] { HitResult.IgnoreHit }, new[] { HitResult.IgnoreMiss, HitResult.ComboBreak })]
public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults) public void TestValidResultPairs(HitResult[] maxResults, HitResult[] minResults)

View File

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

View File

@ -12,6 +12,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; 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); 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() private Func<Player> playToResults()
{ {
var player = playToCompletion(); var player = playToCompletion();

View File

@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
[Test] [Test]
public void TestAddingFlow() public void TestAddingFlow([Values] bool withSystemModActive)
{ {
ModPresetColumn modPresetColumn = null!; ModPresetColumn modPresetColumn = null!;
@ -181,7 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded); AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddAssert("add preset button disabled", () => !this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value); 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); AddAssert("add preset button enabled", () => this.ChildrenOfType<AddPresetButton>().Single().Enabled.Value);
AddStep("click add preset button", () => AddStep("click add preset button", () =>
@ -209,6 +215,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any()); AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
AddUntilStep("preset creation occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 4); 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", () => 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() }); 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); 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] [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("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick()); AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } }); 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) private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)

View File

@ -3,7 +3,9 @@
#nullable disable #nullable disable
using osu.Framework;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -24,6 +26,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null); SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null); SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null); SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
SetDefault(Static.TouchInputActive, RuntimeInfo.IsMobile);
} }
/// <summary> /// <summary>
@ -63,6 +66,12 @@ namespace osu.Game.Configuration
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>). /// 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. /// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary> /// </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 = new RealmKeyBindingStore(realm, keyCombinationProvider);
KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets); KeyBindingStore.Register(globalBindings, RulesetStore.AvailableRulesets);
@ -575,14 +577,14 @@ namespace osu.Game
case JoystickHandler jh: case JoystickHandler jh:
return new JoystickSettings(jh); return new JoystickSettings(jh);
case TouchHandler th:
return new TouchSettings(th);
} }
} }
switch (handler) switch (handler)
{ {
case TouchHandler th:
return new TouchSettings(th);
case MidiHandler: case MidiHandler:
return new InputSection.HandlerSection(handler); return new InputSection.HandlerSection(handler);

View File

@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Mods
{ {
Name = nameTextBox.Current.Value, Name = nameTextBox.Current.Value,
Description = descriptionTextBox.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)! Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)!
})); }));

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -56,17 +55,14 @@ namespace osu.Game.Overlays.Mods
protected override void Select() 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, var selectedSystemMods = selectedMods.Value.Where(mod => mod.Type == ModType.System);
// which will also have the side effect of activating the preset (see `updateActiveState()`). // will also have the side effect of activating the preset (see `updateActiveState()`).
selectedMods.Value = Preset.Value.Mods.ToArray(); selectedMods.Value = Preset.Value.Mods.Concat(selectedSystemMods).ToArray();
} }
protected override void Deselect() 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 selectedMods.Value = selectedMods.Value.Except(Preset.Value.Mods).ToArray();
// (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>();
} }
private void selectedModsChanged() private void selectedModsChanged()
@ -79,7 +75,7 @@ namespace osu.Game.Overlays.Mods
private void updateActiveState() 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 #region Filtering support

View File

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

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input.Handlers; using osu.Framework.Input.Handlers;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -27,12 +28,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager osuConfig) private void load(OsuConfigManager osuConfig)
{
if (!RuntimeInfo.IsMobile) // don't allow disabling the only input method (touch) on mobile.
{ {
Add(new SettingsCheckbox Add(new SettingsCheckbox
{ {
LabelText = CommonStrings.Enabled, LabelText = CommonStrings.Enabled,
Current = handler.Enabled Current = handler.Enabled
}); });
}
Add(new SettingsCheckbox Add(new SettingsCheckbox
{ {

View File

@ -59,6 +59,13 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
bool ValidForMultiplayerAsFreeMod { get; } 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> /// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod. /// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary> /// </summary>

View File

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

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mods
public sealed override bool ValidForMultiplayer => false; public sealed override bool ValidForMultiplayer => false;
public sealed override bool ValidForMultiplayerAsFreeMod => 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; 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 ModAutoplay? GetAutoplayMod() => CreateMod<ModAutoplay>();
public ModTouchDevice? GetTouchDeviceMod() => CreateMod<ModTouchDevice>();
/// <summary> /// <summary>
/// Create a transformer which adds lookups specific to a ruleset to skin sources. /// Create a transformer which adds lookups specific to a ruleset to skin sources.
/// </summary> /// </summary>

View File

@ -350,6 +350,9 @@ namespace osu.Game.Rulesets.Scoring
if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss) if (maxResult.IsBonus() && minResult != HitResult.IgnoreMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement."); throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.IgnoreMiss} is the only valid minimum result for a {maxResult} judgement.");
if (minResult == HitResult.IgnoreMiss)
return;
if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss) if (maxResult == HitResult.LargeTickHit && minResult != HitResult.LargeTickMiss)
throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement."); throw new ArgumentOutOfRangeException(nameof(minResult), $"{HitResult.LargeTickMiss} is the only valid minimum result for a {maxResult} judgement.");

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -22,8 +23,9 @@ namespace osu.Game.Screens.Play.PlayerSettings
{ {
new PlayerCheckbox new PlayerCheckbox
{ {
LabelText = MouseSettingsStrings.DisableClicksDuringGameplay, // TODO: change to touchscreen detection once https://github.com/ppy/osu/pull/25348 makes it in
Current = config.GetBindable<bool>(OsuSetting.MouseDisableButtons) LabelText = RuntimeInfo.IsDesktop ? MouseSettingsStrings.DisableClicksDuringGameplay : TouchSettingsStrings.DisableTapsDuringGameplay,
Current = config.GetBindable<bool>(RuntimeInfo.IsDesktop ? OsuSetting.MouseDisableButtons : OsuSetting.TouchDisableGameplayTaps)
} }
}; };
} }

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() protected override void LoadAsyncComplete()
{ {
base.LoadAsyncComplete(); base.LoadAsyncComplete();

View File

@ -279,6 +279,7 @@ namespace osu.Game.Screens.Select
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
new SongSelectTouchInputDetector()
}); });
if (ShowFooter) 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)) if (!CheckCompatibleSet(mods, out invalidMods))
return false; return false;
return checkValid(mods, m => m.Type != ModType.System && m.HasImplementation, out invalidMods); return checkValid(mods, m => m.HasImplementation, out invalidMods);
} }
/// <summary> /// <summary>